@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,929 @@
|
|
|
1
|
+
"""Tests for pf handoff — Script-First Handoff (Story 105-1).
|
|
2
|
+
|
|
3
|
+
Epic: 105 (Script-First Handoff)
|
|
4
|
+
Story: 105-1 — Create handoff CLI with resolve-gate and complete-phase
|
|
5
|
+
|
|
6
|
+
Tests the two subcommands:
|
|
7
|
+
- resolve-gate: reads workflow YAML, resolves gate, returns RESOLVE_RESULT
|
|
8
|
+
- complete-phase: atomically updates session file with phase transition
|
|
9
|
+
|
|
10
|
+
Acceptance Criteria:
|
|
11
|
+
- [AC1] resolve-gate reads workflow YAML, finds current phase gate,
|
|
12
|
+
checks assessment, returns structured RESOLVE_RESULT
|
|
13
|
+
- [AC2] complete-phase atomically updates session file (temp+mv) with
|
|
14
|
+
phase transition, timestamps, and history tables
|
|
15
|
+
- [AC3] Python module in pennyfarthing_scripts.handoff
|
|
16
|
+
- [AC4] stdout is the only communication channel — no side-channel files
|
|
17
|
+
- [AC5] Exit codes: 0 = ready/skip, 1 = blocked
|
|
18
|
+
- [AC6] YAML parsing via PyYAML
|
|
19
|
+
- [AC7] Session parsing via Python string/regex
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
import textwrap
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from unittest.mock import patch
|
|
28
|
+
|
|
29
|
+
import pytest
|
|
30
|
+
import yaml
|
|
31
|
+
from click.testing import CliRunner
|
|
32
|
+
|
|
33
|
+
from pennyfarthing_scripts.cli import cli
|
|
34
|
+
from pennyfarthing_scripts.handoff.complete_phase import complete_phase
|
|
35
|
+
from pennyfarthing_scripts.handoff.resolve_gate import resolve_gate
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Fixtures: Workflow YAML data
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
TDD_WORKFLOW = {
|
|
42
|
+
"workflow": {
|
|
43
|
+
"name": "tdd",
|
|
44
|
+
"phases": [
|
|
45
|
+
{"name": "setup", "agent": "sm"},
|
|
46
|
+
{"name": "red", "agent": "tea", "gate": {"type": "tests_fail"}},
|
|
47
|
+
{"name": "green", "agent": "dev", "gate": {"type": "tests_pass"}},
|
|
48
|
+
{"name": "review", "agent": "reviewer", "gate": {"type": "approval"}},
|
|
49
|
+
{"name": "finish", "agent": "sm"},
|
|
50
|
+
],
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
TRIVIAL_WORKFLOW = {
|
|
55
|
+
"workflow": {
|
|
56
|
+
"name": "trivial",
|
|
57
|
+
"phases": [
|
|
58
|
+
{"name": "setup", "agent": "sm"},
|
|
59
|
+
{"name": "implement", "agent": "dev", "gate": {"type": "tests_pass"}},
|
|
60
|
+
{"name": "review", "agent": "reviewer", "gate": {"type": "approval"}},
|
|
61
|
+
{"name": "finish", "agent": "sm"},
|
|
62
|
+
],
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
PATCH_WORKFLOW = {
|
|
67
|
+
"workflow": {
|
|
68
|
+
"name": "patch",
|
|
69
|
+
"phases": [
|
|
70
|
+
{"name": "fix", "agent": "dev", "gate": {"type": "manual"}},
|
|
71
|
+
],
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
BDD_WORKFLOW = {
|
|
76
|
+
"workflow": {
|
|
77
|
+
"name": "bdd",
|
|
78
|
+
"phases": [
|
|
79
|
+
{"name": "setup", "agent": "sm"},
|
|
80
|
+
{"name": "design", "agent": "ux-designer", "gate": {"type": "design_review"}},
|
|
81
|
+
{"name": "red", "agent": "tea", "gate": {"type": "tests_fail"}},
|
|
82
|
+
{"name": "green", "agent": "dev", "gate": {"type": "tests_pass"}},
|
|
83
|
+
{"name": "review", "agent": "reviewer", "gate": {"type": "approval"}},
|
|
84
|
+
{"name": "finish", "agent": "sm"},
|
|
85
|
+
],
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Fixtures: Session file content
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
SESSION_WITH_ASSESSMENT = textwrap.dedent("""\
|
|
95
|
+
# Story 105-1: Create handoff-cli with resolve-gate and complete-phase
|
|
96
|
+
|
|
97
|
+
**Story ID:** 105-1
|
|
98
|
+
**Workflow:** tdd
|
|
99
|
+
**Phase:** green
|
|
100
|
+
**Phase Started:** 2026-02-15T07:53:07Z
|
|
101
|
+
|
|
102
|
+
## TEA Assessment
|
|
103
|
+
|
|
104
|
+
**Tests Written:** 5 tests
|
|
105
|
+
**Status:** RED confirmed
|
|
106
|
+
|
|
107
|
+
## Workflow Tracking
|
|
108
|
+
|
|
109
|
+
**Phase:** green
|
|
110
|
+
**Phase Started:** 2026-02-15T07:53:07Z
|
|
111
|
+
|
|
112
|
+
### Phase History
|
|
113
|
+
| Phase | Started | Ended | Duration |
|
|
114
|
+
|-------|---------|-------|----------|
|
|
115
|
+
| setup | 2026-02-15T07:50:00Z | 2026-02-15T07:52:00Z | 2m |
|
|
116
|
+
| red | 2026-02-15T07:52:00Z | 2026-02-15T07:53:07Z | 1m |
|
|
117
|
+
| green | 2026-02-15T07:53:07Z | - | - |
|
|
118
|
+
|
|
119
|
+
### Handoff History
|
|
120
|
+
| From | To | Gate | Status | Timestamp |
|
|
121
|
+
|------|-----|------|--------|-----------|
|
|
122
|
+
| setup (sm) | red (tea) | - | PASSED | 2026-02-15T07:52:00Z |
|
|
123
|
+
| red (tea) | green (dev) | tests_fail | PASSED | 2026-02-15T07:53:07Z |
|
|
124
|
+
""")
|
|
125
|
+
|
|
126
|
+
SESSION_WITHOUT_ASSESSMENT = textwrap.dedent("""\
|
|
127
|
+
# Story 105-1: Create handoff-cli with resolve-gate and complete-phase
|
|
128
|
+
|
|
129
|
+
**Story ID:** 105-1
|
|
130
|
+
**Workflow:** tdd
|
|
131
|
+
**Phase:** green
|
|
132
|
+
**Phase Started:** 2026-02-15T07:53:07Z
|
|
133
|
+
|
|
134
|
+
## Workflow Tracking
|
|
135
|
+
|
|
136
|
+
**Phase:** green
|
|
137
|
+
**Phase Started:** 2026-02-15T07:53:07Z
|
|
138
|
+
|
|
139
|
+
### Phase History
|
|
140
|
+
| Phase | Started | Ended | Duration |
|
|
141
|
+
|-------|---------|-------|----------|
|
|
142
|
+
| green | 2026-02-15T07:53:07Z | - | - |
|
|
143
|
+
|
|
144
|
+
### Handoff History
|
|
145
|
+
| From | To | Gate | Status | Timestamp |
|
|
146
|
+
|------|-----|------|--------|-----------|
|
|
147
|
+
""")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# Fixtures: Project structure
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@pytest.fixture
|
|
156
|
+
def project(tmp_path: Path) -> Path:
|
|
157
|
+
"""Create a minimal project structure with workflow YAMLs."""
|
|
158
|
+
workflows_dir = tmp_path / ".pennyfarthing" / "workflows"
|
|
159
|
+
workflows_dir.mkdir(parents=True)
|
|
160
|
+
|
|
161
|
+
for name, data in [
|
|
162
|
+
("tdd", TDD_WORKFLOW),
|
|
163
|
+
("trivial", TRIVIAL_WORKFLOW),
|
|
164
|
+
("patch", PATCH_WORKFLOW),
|
|
165
|
+
("bdd", BDD_WORKFLOW),
|
|
166
|
+
]:
|
|
167
|
+
(workflows_dir / f"{name}.yaml").write_text(
|
|
168
|
+
yaml.dump(data, default_flow_style=False)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
(tmp_path / ".session").mkdir()
|
|
172
|
+
return tmp_path
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@pytest.fixture
|
|
176
|
+
def session_with_assessment(project: Path) -> Path:
|
|
177
|
+
"""Create a session file that has an assessment section."""
|
|
178
|
+
session_file = project / ".session" / "105-1-session.md"
|
|
179
|
+
session_file.write_text(SESSION_WITH_ASSESSMENT)
|
|
180
|
+
return session_file
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@pytest.fixture
|
|
184
|
+
def session_without_assessment(project: Path) -> Path:
|
|
185
|
+
"""Create a session file without an assessment section."""
|
|
186
|
+
session_file = project / ".session" / "105-1-session.md"
|
|
187
|
+
session_file.write_text(SESSION_WITHOUT_ASSESSMENT)
|
|
188
|
+
return session_file
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@pytest.fixture
|
|
192
|
+
def runner() -> CliRunner:
|
|
193
|
+
"""Create a CLI test runner."""
|
|
194
|
+
return CliRunner()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ===========================================================================
|
|
198
|
+
# AC1: resolve-gate returns structured RESOLVE_RESULT
|
|
199
|
+
# ===========================================================================
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class TestResolveGateReady:
|
|
203
|
+
"""AC1 + AC5: resolve-gate returns 'ready' with correct fields."""
|
|
204
|
+
|
|
205
|
+
def test_tdd_green_phase_returns_ready(
|
|
206
|
+
self, project: Path, session_with_assessment: Path
|
|
207
|
+
) -> None:
|
|
208
|
+
"""AC1: TDD green phase (gate=tests_pass) → status: ready."""
|
|
209
|
+
result = resolve_gate("105-1", "tdd", "green", project_root=project)
|
|
210
|
+
assert result["status"] == "ready"
|
|
211
|
+
|
|
212
|
+
def test_tdd_green_gate_type_is_tests_pass(
|
|
213
|
+
self, project: Path, session_with_assessment: Path
|
|
214
|
+
) -> None:
|
|
215
|
+
"""AC1: TDD green phase gate type should be tests_pass."""
|
|
216
|
+
result = resolve_gate("105-1", "tdd", "green", project_root=project)
|
|
217
|
+
assert result["gate_type"] == "tests_pass"
|
|
218
|
+
|
|
219
|
+
def test_tdd_green_next_agent_is_reviewer(
|
|
220
|
+
self, project: Path, session_with_assessment: Path
|
|
221
|
+
) -> None:
|
|
222
|
+
"""AC1: After TDD green, next agent should be reviewer."""
|
|
223
|
+
result = resolve_gate("105-1", "tdd", "green", project_root=project)
|
|
224
|
+
assert result["next_agent"] == "reviewer"
|
|
225
|
+
|
|
226
|
+
def test_tdd_green_next_phase_is_review(
|
|
227
|
+
self, project: Path, session_with_assessment: Path
|
|
228
|
+
) -> None:
|
|
229
|
+
"""AC1: After TDD green, next phase should be review."""
|
|
230
|
+
result = resolve_gate("105-1", "tdd", "green", project_root=project)
|
|
231
|
+
assert result["next_phase"] == "review"
|
|
232
|
+
|
|
233
|
+
def test_tdd_red_phase_returns_ready(
|
|
234
|
+
self, project: Path, session_with_assessment: Path
|
|
235
|
+
) -> None:
|
|
236
|
+
"""AC1: TDD red phase (gate=tests_fail) → status: ready."""
|
|
237
|
+
# Rewrite session to be in red phase with assessment
|
|
238
|
+
session_file = project / ".session" / "105-1-session.md"
|
|
239
|
+
content = session_file.read_text().replace(
|
|
240
|
+
"**Phase:** green", "**Phase:** red"
|
|
241
|
+
)
|
|
242
|
+
session_file.write_text(content)
|
|
243
|
+
result = resolve_gate("105-1", "tdd", "red", project_root=project)
|
|
244
|
+
assert result["status"] == "ready"
|
|
245
|
+
assert result["gate_type"] == "tests_fail"
|
|
246
|
+
assert result["next_agent"] == "dev"
|
|
247
|
+
assert result["next_phase"] == "green"
|
|
248
|
+
|
|
249
|
+
def test_trivial_implement_returns_ready(
|
|
250
|
+
self, project: Path, session_with_assessment: Path
|
|
251
|
+
) -> None:
|
|
252
|
+
"""AC1: Trivial implement (gate=tests_pass) → status: ready."""
|
|
253
|
+
result = resolve_gate("105-1", "trivial", "implement", project_root=project)
|
|
254
|
+
assert result["status"] == "ready"
|
|
255
|
+
assert result["gate_type"] == "tests_pass"
|
|
256
|
+
assert result["next_agent"] == "reviewer"
|
|
257
|
+
|
|
258
|
+
def test_bdd_design_returns_ready(
|
|
259
|
+
self, project: Path, session_with_assessment: Path
|
|
260
|
+
) -> None:
|
|
261
|
+
"""AC1: BDD design phase (gate=design_review) → ready."""
|
|
262
|
+
result = resolve_gate("105-1", "bdd", "design", project_root=project)
|
|
263
|
+
assert result["status"] == "ready"
|
|
264
|
+
assert result["gate_type"] == "design_review"
|
|
265
|
+
assert result["next_agent"] == "tea"
|
|
266
|
+
assert result["next_phase"] == "red"
|
|
267
|
+
|
|
268
|
+
def test_assessment_found_is_true(
|
|
269
|
+
self, project: Path, session_with_assessment: Path
|
|
270
|
+
) -> None:
|
|
271
|
+
"""AC1: assessment_found should be True when assessment exists."""
|
|
272
|
+
result = resolve_gate("105-1", "tdd", "green", project_root=project)
|
|
273
|
+
assert result["assessment_found"] is True
|
|
274
|
+
|
|
275
|
+
def test_exit_code_zero_when_ready(
|
|
276
|
+
self, project: Path, session_with_assessment: Path, runner: CliRunner
|
|
277
|
+
) -> None:
|
|
278
|
+
"""AC5: Exit code 0 when gate resolves to ready."""
|
|
279
|
+
with patch(
|
|
280
|
+
"pennyfarthing_scripts.handoff.resolve_gate.resolve_gate",
|
|
281
|
+
return_value={
|
|
282
|
+
"status": "ready",
|
|
283
|
+
"gate_type": "tests_pass",
|
|
284
|
+
"gate_file": None,
|
|
285
|
+
"next_agent": "reviewer",
|
|
286
|
+
"next_phase": "review",
|
|
287
|
+
"assessment_found": True,
|
|
288
|
+
"error": None,
|
|
289
|
+
},
|
|
290
|
+
):
|
|
291
|
+
result = runner.invoke(
|
|
292
|
+
cli, ["handoff", "resolve-gate", "105-1", "tdd", "green"]
|
|
293
|
+
)
|
|
294
|
+
assert result.exit_code == 0
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class TestResolveGateBlocked:
|
|
298
|
+
"""AC1 + AC5: resolve-gate returns 'blocked' when no assessment."""
|
|
299
|
+
|
|
300
|
+
def test_missing_assessment_returns_blocked(
|
|
301
|
+
self, project: Path, session_without_assessment: Path
|
|
302
|
+
) -> None:
|
|
303
|
+
"""AC1: Missing assessment section → status: blocked."""
|
|
304
|
+
result = resolve_gate("105-1", "tdd", "green", project_root=project)
|
|
305
|
+
assert result["status"] == "blocked"
|
|
306
|
+
|
|
307
|
+
def test_blocked_has_assessment_found_false(
|
|
308
|
+
self, project: Path, session_without_assessment: Path
|
|
309
|
+
) -> None:
|
|
310
|
+
"""AC1: Blocked result should have assessment_found: False."""
|
|
311
|
+
result = resolve_gate("105-1", "tdd", "green", project_root=project)
|
|
312
|
+
assert result["assessment_found"] is False
|
|
313
|
+
|
|
314
|
+
def test_blocked_exit_code_one(self, runner: CliRunner) -> None:
|
|
315
|
+
"""AC5: Exit code 1 when gate resolves to blocked."""
|
|
316
|
+
with patch(
|
|
317
|
+
"pennyfarthing_scripts.handoff.resolve_gate.resolve_gate",
|
|
318
|
+
return_value={
|
|
319
|
+
"status": "blocked",
|
|
320
|
+
"gate_type": "tests_pass",
|
|
321
|
+
"gate_file": None,
|
|
322
|
+
"next_agent": "reviewer",
|
|
323
|
+
"next_phase": "review",
|
|
324
|
+
"assessment_found": False,
|
|
325
|
+
"error": None,
|
|
326
|
+
},
|
|
327
|
+
):
|
|
328
|
+
result = runner.invoke(
|
|
329
|
+
cli, ["handoff", "resolve-gate", "105-1", "tdd", "green"]
|
|
330
|
+
)
|
|
331
|
+
assert result.exit_code == 1
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class TestResolveGateSkip:
|
|
335
|
+
"""AC1: resolve-gate returns 'skip' for manual gates."""
|
|
336
|
+
|
|
337
|
+
def test_manual_gate_returns_skip(
|
|
338
|
+
self, project: Path, session_with_assessment: Path
|
|
339
|
+
) -> None:
|
|
340
|
+
"""AC1: Patch fix phase (gate=manual) → status: skip."""
|
|
341
|
+
result = resolve_gate("105-1", "patch", "fix", project_root=project)
|
|
342
|
+
assert result["status"] == "skip"
|
|
343
|
+
|
|
344
|
+
def test_manual_gate_type_is_manual(
|
|
345
|
+
self, project: Path, session_with_assessment: Path
|
|
346
|
+
) -> None:
|
|
347
|
+
"""AC1: Skip result should have gate_type: manual."""
|
|
348
|
+
result = resolve_gate("105-1", "patch", "fix", project_root=project)
|
|
349
|
+
assert result["gate_type"] == "manual"
|
|
350
|
+
|
|
351
|
+
def test_skip_exit_code_zero(self, runner: CliRunner) -> None:
|
|
352
|
+
"""AC5: Exit code 0 for skip status."""
|
|
353
|
+
with patch(
|
|
354
|
+
"pennyfarthing_scripts.handoff.resolve_gate.resolve_gate",
|
|
355
|
+
return_value={
|
|
356
|
+
"status": "skip",
|
|
357
|
+
"gate_type": "manual",
|
|
358
|
+
"gate_file": None,
|
|
359
|
+
"next_agent": None,
|
|
360
|
+
"next_phase": None,
|
|
361
|
+
"assessment_found": True,
|
|
362
|
+
"error": None,
|
|
363
|
+
},
|
|
364
|
+
):
|
|
365
|
+
result = runner.invoke(
|
|
366
|
+
cli, ["handoff", "resolve-gate", "105-1", "patch", "fix"]
|
|
367
|
+
)
|
|
368
|
+
assert result.exit_code == 0
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class TestResolveGateErrors:
|
|
372
|
+
"""AC1: resolve-gate returns error for invalid inputs."""
|
|
373
|
+
|
|
374
|
+
def test_invalid_workflow_returns_error(self, project: Path) -> None:
|
|
375
|
+
"""AC1: Unknown workflow → error result."""
|
|
376
|
+
result = resolve_gate("105-1", "nonexistent", "green", project_root=project)
|
|
377
|
+
assert result["status"] == "error" or result.get("error") is not None
|
|
378
|
+
|
|
379
|
+
def test_invalid_phase_returns_error(
|
|
380
|
+
self, project: Path, session_with_assessment: Path
|
|
381
|
+
) -> None:
|
|
382
|
+
"""AC1: Unknown phase → error result."""
|
|
383
|
+
result = resolve_gate("105-1", "tdd", "nonexistent", project_root=project)
|
|
384
|
+
assert result["status"] == "error" or result.get("error") is not None
|
|
385
|
+
|
|
386
|
+
def test_missing_session_file_returns_blocked(self, project: Path) -> None:
|
|
387
|
+
"""AC1: No session file → blocked (can't check assessment)."""
|
|
388
|
+
result = resolve_gate("105-1", "tdd", "green", project_root=project)
|
|
389
|
+
assert result["status"] in ("blocked", "error")
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
class TestResolveGateOutputContract:
|
|
393
|
+
"""AC1: RESOLVE_RESULT must contain all specified fields."""
|
|
394
|
+
|
|
395
|
+
REQUIRED_FIELDS = [
|
|
396
|
+
"status",
|
|
397
|
+
"gate_type",
|
|
398
|
+
"gate_file",
|
|
399
|
+
"next_agent",
|
|
400
|
+
"next_phase",
|
|
401
|
+
"assessment_found",
|
|
402
|
+
"error",
|
|
403
|
+
]
|
|
404
|
+
|
|
405
|
+
def test_ready_result_has_all_fields(
|
|
406
|
+
self, project: Path, session_with_assessment: Path
|
|
407
|
+
) -> None:
|
|
408
|
+
"""AC1: Ready result must contain all RESOLVE_RESULT fields."""
|
|
409
|
+
result = resolve_gate("105-1", "tdd", "green", project_root=project)
|
|
410
|
+
for field in self.REQUIRED_FIELDS:
|
|
411
|
+
assert field in result, f"Missing field: {field}"
|
|
412
|
+
|
|
413
|
+
def test_blocked_result_has_all_fields(
|
|
414
|
+
self, project: Path, session_without_assessment: Path
|
|
415
|
+
) -> None:
|
|
416
|
+
"""AC1: Blocked result must contain all RESOLVE_RESULT fields."""
|
|
417
|
+
result = resolve_gate("105-1", "tdd", "green", project_root=project)
|
|
418
|
+
for field in self.REQUIRED_FIELDS:
|
|
419
|
+
assert field in result, f"Missing field: {field}"
|
|
420
|
+
|
|
421
|
+
def test_skip_result_has_all_fields(
|
|
422
|
+
self, project: Path, session_with_assessment: Path
|
|
423
|
+
) -> None:
|
|
424
|
+
"""AC1: Skip result must contain all RESOLVE_RESULT fields."""
|
|
425
|
+
result = resolve_gate("105-1", "patch", "fix", project_root=project)
|
|
426
|
+
for field in self.REQUIRED_FIELDS:
|
|
427
|
+
assert field in result, f"Missing field: {field}"
|
|
428
|
+
|
|
429
|
+
def test_gate_file_is_null_for_mvp(
|
|
430
|
+
self, project: Path, session_with_assessment: Path
|
|
431
|
+
) -> None:
|
|
432
|
+
"""AC1: gate_file should be None for MVP (populated in epic 106)."""
|
|
433
|
+
result = resolve_gate("105-1", "tdd", "green", project_root=project)
|
|
434
|
+
assert result["gate_file"] is None
|
|
435
|
+
|
|
436
|
+
def test_error_is_null_on_success(
|
|
437
|
+
self, project: Path, session_with_assessment: Path
|
|
438
|
+
) -> None:
|
|
439
|
+
"""AC1: error should be None when resolve succeeds."""
|
|
440
|
+
result = resolve_gate("105-1", "tdd", "green", project_root=project)
|
|
441
|
+
assert result["error"] is None
|
|
442
|
+
|
|
443
|
+
def test_status_is_valid_value(
|
|
444
|
+
self, project: Path, session_with_assessment: Path
|
|
445
|
+
) -> None:
|
|
446
|
+
"""AC1: status must be one of: ready, blocked, skip."""
|
|
447
|
+
result = resolve_gate("105-1", "tdd", "green", project_root=project)
|
|
448
|
+
assert result["status"] in ("ready", "blocked", "skip")
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class TestResolveGateCLIOutput:
|
|
452
|
+
"""AC4 + AC6: CLI outputs valid YAML to stdout."""
|
|
453
|
+
|
|
454
|
+
def test_cli_output_is_valid_yaml(self, runner: CliRunner) -> None:
|
|
455
|
+
"""AC6: CLI output should be parseable YAML."""
|
|
456
|
+
with patch(
|
|
457
|
+
"pennyfarthing_scripts.handoff.resolve_gate.resolve_gate",
|
|
458
|
+
return_value={
|
|
459
|
+
"status": "ready",
|
|
460
|
+
"gate_type": "tests_pass",
|
|
461
|
+
"gate_file": None,
|
|
462
|
+
"next_agent": "reviewer",
|
|
463
|
+
"next_phase": "review",
|
|
464
|
+
"assessment_found": True,
|
|
465
|
+
"error": None,
|
|
466
|
+
},
|
|
467
|
+
):
|
|
468
|
+
result = runner.invoke(
|
|
469
|
+
cli, ["handoff", "resolve-gate", "105-1", "tdd", "green"]
|
|
470
|
+
)
|
|
471
|
+
parsed = yaml.safe_load(result.output)
|
|
472
|
+
assert "RESOLVE_RESULT" in parsed
|
|
473
|
+
|
|
474
|
+
def test_cli_output_wraps_in_resolve_result_key(self, runner: CliRunner) -> None:
|
|
475
|
+
"""AC4: Output should be wrapped in RESOLVE_RESULT key."""
|
|
476
|
+
with patch(
|
|
477
|
+
"pennyfarthing_scripts.handoff.resolve_gate.resolve_gate",
|
|
478
|
+
return_value={
|
|
479
|
+
"status": "ready",
|
|
480
|
+
"gate_type": "tests_pass",
|
|
481
|
+
"gate_file": None,
|
|
482
|
+
"next_agent": "reviewer",
|
|
483
|
+
"next_phase": "review",
|
|
484
|
+
"assessment_found": True,
|
|
485
|
+
"error": None,
|
|
486
|
+
},
|
|
487
|
+
):
|
|
488
|
+
result = runner.invoke(
|
|
489
|
+
cli, ["handoff", "resolve-gate", "105-1", "tdd", "green"]
|
|
490
|
+
)
|
|
491
|
+
parsed = yaml.safe_load(result.output)
|
|
492
|
+
assert parsed["RESOLVE_RESULT"]["status"] == "ready"
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
# ===========================================================================
|
|
496
|
+
# AC2: complete-phase atomically updates session file
|
|
497
|
+
# ===========================================================================
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
class TestCompletePhaseUpdatesSession:
|
|
501
|
+
"""AC2: complete-phase updates session with phase transition."""
|
|
502
|
+
|
|
503
|
+
def test_updates_phase_line(
|
|
504
|
+
self, project: Path, session_with_assessment: Path
|
|
505
|
+
) -> None:
|
|
506
|
+
"""AC2: **Phase:** line should change to the new phase."""
|
|
507
|
+
complete_phase(
|
|
508
|
+
"105-1", "tdd", "green", "review", "tests_pass", project_root=project
|
|
509
|
+
)
|
|
510
|
+
content = session_with_assessment.read_text()
|
|
511
|
+
# The Phase line in the Workflow Tracking section should be "review"
|
|
512
|
+
assert "**Phase:** review" in content
|
|
513
|
+
|
|
514
|
+
def test_phase_line_no_longer_has_old_value(
|
|
515
|
+
self, project: Path, session_with_assessment: Path
|
|
516
|
+
) -> None:
|
|
517
|
+
"""AC2: Old phase value should not appear in **Phase:** line."""
|
|
518
|
+
complete_phase(
|
|
519
|
+
"105-1", "tdd", "green", "review", "tests_pass", project_root=project
|
|
520
|
+
)
|
|
521
|
+
content = session_with_assessment.read_text()
|
|
522
|
+
# Should not have the old phase value on a Phase line
|
|
523
|
+
phase_lines = [
|
|
524
|
+
line for line in content.splitlines() if line.startswith("**Phase:**")
|
|
525
|
+
]
|
|
526
|
+
for line in phase_lines:
|
|
527
|
+
assert "green" not in line
|
|
528
|
+
|
|
529
|
+
def test_updates_phase_started_timestamp(
|
|
530
|
+
self, project: Path, session_with_assessment: Path
|
|
531
|
+
) -> None:
|
|
532
|
+
"""AC2: **Phase Started:** should have a new ISO timestamp."""
|
|
533
|
+
complete_phase(
|
|
534
|
+
"105-1", "tdd", "green", "review", "tests_pass", project_root=project
|
|
535
|
+
)
|
|
536
|
+
content = session_with_assessment.read_text()
|
|
537
|
+
# Find Phase Started lines — at least one should NOT be the old timestamp
|
|
538
|
+
started_lines = [
|
|
539
|
+
line
|
|
540
|
+
for line in content.splitlines()
|
|
541
|
+
if line.startswith("**Phase Started:**")
|
|
542
|
+
]
|
|
543
|
+
assert len(started_lines) > 0
|
|
544
|
+
# At least one should have a different timestamp than the original
|
|
545
|
+
assert any("2026-02-15T07:53:07Z" not in line for line in started_lines)
|
|
546
|
+
|
|
547
|
+
def test_fills_ended_in_phase_history(
|
|
548
|
+
self, project: Path, session_with_assessment: Path
|
|
549
|
+
) -> None:
|
|
550
|
+
"""AC2: Current phase row in Phase History should get Ended timestamp."""
|
|
551
|
+
complete_phase(
|
|
552
|
+
"105-1", "tdd", "green", "review", "tests_pass", project_root=project
|
|
553
|
+
)
|
|
554
|
+
content = session_with_assessment.read_text()
|
|
555
|
+
# The green phase row should now have an Ended value (not just "- |")
|
|
556
|
+
# Find the row that starts with "| green"
|
|
557
|
+
green_rows = [
|
|
558
|
+
line
|
|
559
|
+
for line in content.splitlines()
|
|
560
|
+
if line.strip().startswith("| green")
|
|
561
|
+
]
|
|
562
|
+
assert len(green_rows) > 0
|
|
563
|
+
# The Ended column (3rd) should not be "-"
|
|
564
|
+
green_row = green_rows[0]
|
|
565
|
+
cols = [c.strip() for c in green_row.split("|") if c.strip()]
|
|
566
|
+
assert len(cols) >= 4, f"Expected 4+ columns, got: {cols}"
|
|
567
|
+
ended = cols[2] # Phase | Started | Ended | Duration
|
|
568
|
+
assert ended != "-", f"Ended should be filled, got: {ended}"
|
|
569
|
+
|
|
570
|
+
def test_fills_duration_in_phase_history(
|
|
571
|
+
self, project: Path, session_with_assessment: Path
|
|
572
|
+
) -> None:
|
|
573
|
+
"""AC2: Current phase row should get Duration calculated."""
|
|
574
|
+
complete_phase(
|
|
575
|
+
"105-1", "tdd", "green", "review", "tests_pass", project_root=project
|
|
576
|
+
)
|
|
577
|
+
content = session_with_assessment.read_text()
|
|
578
|
+
green_rows = [
|
|
579
|
+
line
|
|
580
|
+
for line in content.splitlines()
|
|
581
|
+
if line.strip().startswith("| green")
|
|
582
|
+
]
|
|
583
|
+
assert len(green_rows) > 0
|
|
584
|
+
green_row = green_rows[0]
|
|
585
|
+
cols = [c.strip() for c in green_row.split("|") if c.strip()]
|
|
586
|
+
duration = cols[3] # Phase | Started | Ended | Duration
|
|
587
|
+
assert duration != "-", f"Duration should be filled, got: {duration}"
|
|
588
|
+
|
|
589
|
+
def test_adds_new_phase_row(
|
|
590
|
+
self, project: Path, session_with_assessment: Path
|
|
591
|
+
) -> None:
|
|
592
|
+
"""AC2: A new row for the next phase should appear in Phase History."""
|
|
593
|
+
complete_phase(
|
|
594
|
+
"105-1", "tdd", "green", "review", "tests_pass", project_root=project
|
|
595
|
+
)
|
|
596
|
+
content = session_with_assessment.read_text()
|
|
597
|
+
review_rows = [
|
|
598
|
+
line
|
|
599
|
+
for line in content.splitlines()
|
|
600
|
+
if line.strip().startswith("| review")
|
|
601
|
+
]
|
|
602
|
+
assert len(review_rows) > 0, "New 'review' phase row not found in Phase History"
|
|
603
|
+
|
|
604
|
+
def test_adds_handoff_history_row(
|
|
605
|
+
self, project: Path, session_with_assessment: Path
|
|
606
|
+
) -> None:
|
|
607
|
+
"""AC2: A new row should be added to Handoff History table."""
|
|
608
|
+
complete_phase(
|
|
609
|
+
"105-1", "tdd", "green", "review", "tests_pass", project_root=project
|
|
610
|
+
)
|
|
611
|
+
content = session_with_assessment.read_text()
|
|
612
|
+
# Should have a row mentioning green → review with tests_pass
|
|
613
|
+
handoff_rows = [
|
|
614
|
+
line
|
|
615
|
+
for line in content.splitlines()
|
|
616
|
+
if "green" in line and "review" in line and "tests_pass" in line
|
|
617
|
+
]
|
|
618
|
+
assert len(handoff_rows) > 0, "Handoff History row for green→review not found"
|
|
619
|
+
|
|
620
|
+
def test_handoff_history_includes_passed_status(
|
|
621
|
+
self, project: Path, session_with_assessment: Path
|
|
622
|
+
) -> None:
|
|
623
|
+
"""AC2: Handoff History row should include PASSED status."""
|
|
624
|
+
complete_phase(
|
|
625
|
+
"105-1", "tdd", "green", "review", "tests_pass", project_root=project
|
|
626
|
+
)
|
|
627
|
+
content = session_with_assessment.read_text()
|
|
628
|
+
# Find the new handoff row (should be the last one with "review")
|
|
629
|
+
lines = content.splitlines()
|
|
630
|
+
handoff_section = False
|
|
631
|
+
handoff_rows = []
|
|
632
|
+
for line in lines:
|
|
633
|
+
if "### Handoff History" in line:
|
|
634
|
+
handoff_section = True
|
|
635
|
+
continue
|
|
636
|
+
if handoff_section and line.strip().startswith("|") and "---" not in line:
|
|
637
|
+
if "From" not in line: # Skip header
|
|
638
|
+
handoff_rows.append(line)
|
|
639
|
+
# Last row should have PASSED
|
|
640
|
+
assert len(handoff_rows) > 0
|
|
641
|
+
last_row = handoff_rows[-1]
|
|
642
|
+
assert "PASSED" in last_row
|
|
643
|
+
|
|
644
|
+
def test_handoff_history_includes_timestamp(
|
|
645
|
+
self, project: Path, session_with_assessment: Path
|
|
646
|
+
) -> None:
|
|
647
|
+
"""AC2: Handoff History row should include an ISO timestamp."""
|
|
648
|
+
complete_phase(
|
|
649
|
+
"105-1", "tdd", "green", "review", "tests_pass", project_root=project
|
|
650
|
+
)
|
|
651
|
+
content = session_with_assessment.read_text()
|
|
652
|
+
lines = content.splitlines()
|
|
653
|
+
handoff_section = False
|
|
654
|
+
handoff_rows = []
|
|
655
|
+
for line in lines:
|
|
656
|
+
if "### Handoff History" in line:
|
|
657
|
+
handoff_section = True
|
|
658
|
+
continue
|
|
659
|
+
if handoff_section and line.strip().startswith("|") and "---" not in line:
|
|
660
|
+
if "From" not in line:
|
|
661
|
+
handoff_rows.append(line)
|
|
662
|
+
assert len(handoff_rows) > 0
|
|
663
|
+
last_row = handoff_rows[-1]
|
|
664
|
+
# Should contain ISO timestamp pattern
|
|
665
|
+
assert re.search(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}", last_row)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
class TestCompletePhaseAtomicity:
|
|
669
|
+
"""AC2: Session update must be atomic (temp + mv)."""
|
|
670
|
+
|
|
671
|
+
def test_session_file_exists_after_update(
|
|
672
|
+
self, project: Path, session_with_assessment: Path
|
|
673
|
+
) -> None:
|
|
674
|
+
"""AC2: Session file should still exist after update."""
|
|
675
|
+
complete_phase(
|
|
676
|
+
"105-1", "tdd", "green", "review", "tests_pass", project_root=project
|
|
677
|
+
)
|
|
678
|
+
assert session_with_assessment.exists()
|
|
679
|
+
|
|
680
|
+
def test_no_temp_files_left_behind(
|
|
681
|
+
self, project: Path, session_with_assessment: Path
|
|
682
|
+
) -> None:
|
|
683
|
+
"""AC2: No .tmp or temporary files should remain in .session/."""
|
|
684
|
+
complete_phase(
|
|
685
|
+
"105-1", "tdd", "green", "review", "tests_pass", project_root=project
|
|
686
|
+
)
|
|
687
|
+
session_dir = project / ".session"
|
|
688
|
+
temp_files = list(session_dir.glob("*.tmp")) + list(session_dir.glob("*.bak"))
|
|
689
|
+
assert len(temp_files) == 0, f"Temp files left behind: {temp_files}"
|
|
690
|
+
|
|
691
|
+
def test_session_content_is_valid_after_update(
|
|
692
|
+
self, project: Path, session_with_assessment: Path
|
|
693
|
+
) -> None:
|
|
694
|
+
"""AC2: Session file should still be valid markdown after update."""
|
|
695
|
+
complete_phase(
|
|
696
|
+
"105-1", "tdd", "green", "review", "tests_pass", project_root=project
|
|
697
|
+
)
|
|
698
|
+
content = session_with_assessment.read_text()
|
|
699
|
+
# Should still have required sections
|
|
700
|
+
assert "# Story 105-1" in content
|
|
701
|
+
assert "## Workflow Tracking" in content
|
|
702
|
+
assert "### Phase History" in content
|
|
703
|
+
assert "### Handoff History" in content
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
class TestCompletePhaseOutputContract:
|
|
707
|
+
"""AC2: COMPLETE_RESULT must contain all specified fields."""
|
|
708
|
+
|
|
709
|
+
def test_returns_success_status(
|
|
710
|
+
self, project: Path, session_with_assessment: Path
|
|
711
|
+
) -> None:
|
|
712
|
+
"""AC2: Successful update should return status: success."""
|
|
713
|
+
result = complete_phase(
|
|
714
|
+
"105-1", "tdd", "green", "review", "tests_pass", project_root=project
|
|
715
|
+
)
|
|
716
|
+
assert result["status"] == "success"
|
|
717
|
+
|
|
718
|
+
def test_returns_session_file_path(
|
|
719
|
+
self, project: Path, session_with_assessment: Path
|
|
720
|
+
) -> None:
|
|
721
|
+
"""AC2: Result should include the session file path."""
|
|
722
|
+
result = complete_phase(
|
|
723
|
+
"105-1", "tdd", "green", "review", "tests_pass", project_root=project
|
|
724
|
+
)
|
|
725
|
+
assert "session_file" in result
|
|
726
|
+
assert "105-1-session.md" in result["session_file"]
|
|
727
|
+
|
|
728
|
+
def test_returns_null_error_on_success(
|
|
729
|
+
self, project: Path, session_with_assessment: Path
|
|
730
|
+
) -> None:
|
|
731
|
+
"""AC2: error should be None on success."""
|
|
732
|
+
result = complete_phase(
|
|
733
|
+
"105-1", "tdd", "green", "review", "tests_pass", project_root=project
|
|
734
|
+
)
|
|
735
|
+
assert result["error"] is None
|
|
736
|
+
|
|
737
|
+
def test_cli_output_is_valid_yaml(self, runner: CliRunner) -> None:
|
|
738
|
+
"""AC6: CLI output should be parseable YAML."""
|
|
739
|
+
with patch(
|
|
740
|
+
"pennyfarthing_scripts.handoff.complete_phase.complete_phase",
|
|
741
|
+
return_value={
|
|
742
|
+
"status": "success",
|
|
743
|
+
"session_file": ".session/105-1-session.md",
|
|
744
|
+
"error": None,
|
|
745
|
+
},
|
|
746
|
+
):
|
|
747
|
+
result = runner.invoke(
|
|
748
|
+
cli,
|
|
749
|
+
[
|
|
750
|
+
"handoff",
|
|
751
|
+
"complete-phase",
|
|
752
|
+
"105-1",
|
|
753
|
+
"tdd",
|
|
754
|
+
"green",
|
|
755
|
+
"review",
|
|
756
|
+
"tests_pass",
|
|
757
|
+
],
|
|
758
|
+
)
|
|
759
|
+
assert result.exit_code == 0
|
|
760
|
+
parsed = yaml.safe_load(result.output)
|
|
761
|
+
assert "COMPLETE_RESULT" in parsed
|
|
762
|
+
|
|
763
|
+
def test_cli_exit_code_zero_on_success(self, runner: CliRunner) -> None:
|
|
764
|
+
"""AC5: Exit code 0 on successful phase completion."""
|
|
765
|
+
with patch(
|
|
766
|
+
"pennyfarthing_scripts.handoff.complete_phase.complete_phase",
|
|
767
|
+
return_value={
|
|
768
|
+
"status": "success",
|
|
769
|
+
"session_file": ".session/105-1-session.md",
|
|
770
|
+
"error": None,
|
|
771
|
+
},
|
|
772
|
+
):
|
|
773
|
+
result = runner.invoke(
|
|
774
|
+
cli,
|
|
775
|
+
[
|
|
776
|
+
"handoff",
|
|
777
|
+
"complete-phase",
|
|
778
|
+
"105-1",
|
|
779
|
+
"tdd",
|
|
780
|
+
"green",
|
|
781
|
+
"review",
|
|
782
|
+
"tests_pass",
|
|
783
|
+
],
|
|
784
|
+
)
|
|
785
|
+
assert result.exit_code == 0
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
class TestCompletePhaseErrors:
|
|
789
|
+
"""AC2: Error handling for complete-phase."""
|
|
790
|
+
|
|
791
|
+
def test_missing_session_file_returns_error(self, project: Path) -> None:
|
|
792
|
+
"""AC2: No session file → error result."""
|
|
793
|
+
result = complete_phase(
|
|
794
|
+
"105-1", "tdd", "green", "review", "tests_pass", project_root=project
|
|
795
|
+
)
|
|
796
|
+
assert result["status"] == "error"
|
|
797
|
+
assert result["error"] is not None
|
|
798
|
+
|
|
799
|
+
def test_error_exit_code_one(self, runner: CliRunner) -> None:
|
|
800
|
+
"""AC5: Exit code 1 on error."""
|
|
801
|
+
with patch(
|
|
802
|
+
"pennyfarthing_scripts.handoff.complete_phase.complete_phase",
|
|
803
|
+
return_value={
|
|
804
|
+
"status": "error",
|
|
805
|
+
"session_file": None,
|
|
806
|
+
"error": "Session file not found",
|
|
807
|
+
},
|
|
808
|
+
):
|
|
809
|
+
result = runner.invoke(
|
|
810
|
+
cli,
|
|
811
|
+
[
|
|
812
|
+
"handoff",
|
|
813
|
+
"complete-phase",
|
|
814
|
+
"105-1",
|
|
815
|
+
"tdd",
|
|
816
|
+
"green",
|
|
817
|
+
"review",
|
|
818
|
+
"tests_pass",
|
|
819
|
+
],
|
|
820
|
+
)
|
|
821
|
+
assert result.exit_code == 1
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
# ===========================================================================
|
|
825
|
+
# AC4: stdout is the only communication channel
|
|
826
|
+
# ===========================================================================
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
class TestStdoutOnlyCommunication:
|
|
830
|
+
"""AC4: No side-channel files created during execution."""
|
|
831
|
+
|
|
832
|
+
def test_resolve_gate_no_side_files(
|
|
833
|
+
self, project: Path, session_with_assessment: Path
|
|
834
|
+
) -> None:
|
|
835
|
+
"""AC4: resolve-gate should not create any files."""
|
|
836
|
+
files_before = set(project.rglob("*"))
|
|
837
|
+
resolve_gate("105-1", "tdd", "green", project_root=project)
|
|
838
|
+
files_after = set(project.rglob("*"))
|
|
839
|
+
new_files = files_after - files_before
|
|
840
|
+
assert len(new_files) == 0, f"Unexpected files created: {new_files}"
|
|
841
|
+
|
|
842
|
+
def test_complete_phase_only_modifies_session(
|
|
843
|
+
self, project: Path, session_with_assessment: Path
|
|
844
|
+
) -> None:
|
|
845
|
+
"""AC4: complete-phase should only modify the session file."""
|
|
846
|
+
# Record all files and their mtimes
|
|
847
|
+
files_before = {
|
|
848
|
+
p: p.stat().st_mtime for p in project.rglob("*") if p.is_file()
|
|
849
|
+
}
|
|
850
|
+
complete_phase(
|
|
851
|
+
"105-1", "tdd", "green", "review", "tests_pass", project_root=project
|
|
852
|
+
)
|
|
853
|
+
files_after = {
|
|
854
|
+
p: p.stat().st_mtime for p in project.rglob("*") if p.is_file()
|
|
855
|
+
}
|
|
856
|
+
# Only the session file should be modified
|
|
857
|
+
changed = {
|
|
858
|
+
p
|
|
859
|
+
for p, mtime in files_after.items()
|
|
860
|
+
if files_before.get(p) != mtime
|
|
861
|
+
}
|
|
862
|
+
new = set(files_after) - set(files_before)
|
|
863
|
+
all_changes = changed | new
|
|
864
|
+
allowed = {session_with_assessment}
|
|
865
|
+
unexpected = all_changes - allowed
|
|
866
|
+
assert len(unexpected) == 0, f"Unexpected file changes: {unexpected}"
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
# ===========================================================================
|
|
870
|
+
# AC3: CLI command group exists and is invokable
|
|
871
|
+
# ===========================================================================
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
class TestHandoffCommandExists:
|
|
875
|
+
"""AC3: pf handoff command group with subcommands."""
|
|
876
|
+
|
|
877
|
+
def test_handoff_help(self, runner: CliRunner) -> None:
|
|
878
|
+
"""AC3: pf handoff --help should work."""
|
|
879
|
+
result = runner.invoke(cli, ["handoff", "--help"])
|
|
880
|
+
assert result.exit_code == 0
|
|
881
|
+
assert "resolve-gate" in result.output
|
|
882
|
+
assert "complete-phase" in result.output
|
|
883
|
+
|
|
884
|
+
def test_resolve_gate_help(self, runner: CliRunner) -> None:
|
|
885
|
+
"""AC3: pf handoff resolve-gate --help should work."""
|
|
886
|
+
result = runner.invoke(cli, ["handoff", "resolve-gate", "--help"])
|
|
887
|
+
assert result.exit_code == 0
|
|
888
|
+
assert "STORY_ID" in result.output
|
|
889
|
+
|
|
890
|
+
def test_complete_phase_help(self, runner: CliRunner) -> None:
|
|
891
|
+
"""AC3: pf handoff complete-phase --help should work."""
|
|
892
|
+
result = runner.invoke(cli, ["handoff", "complete-phase", "--help"])
|
|
893
|
+
assert result.exit_code == 0
|
|
894
|
+
assert "STORY_ID" in result.output
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
# ===========================================================================
|
|
898
|
+
# Cross-workflow: resolve-gate works with multiple workflow types
|
|
899
|
+
# ===========================================================================
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
class TestResolveGateMultipleWorkflows:
|
|
903
|
+
"""Verify resolve-gate handles the gate types across all workflows."""
|
|
904
|
+
|
|
905
|
+
def test_tdd_review_gate_is_approval(
|
|
906
|
+
self, project: Path, session_with_assessment: Path
|
|
907
|
+
) -> None:
|
|
908
|
+
"""TDD review phase gate should be 'approval'."""
|
|
909
|
+
result = resolve_gate("105-1", "tdd", "review", project_root=project)
|
|
910
|
+
assert result["gate_type"] == "approval"
|
|
911
|
+
assert result["next_agent"] == "sm"
|
|
912
|
+
assert result["next_phase"] == "finish"
|
|
913
|
+
|
|
914
|
+
def test_trivial_review_gate_is_approval(
|
|
915
|
+
self, project: Path, session_with_assessment: Path
|
|
916
|
+
) -> None:
|
|
917
|
+
"""Trivial review phase gate should be 'approval'."""
|
|
918
|
+
result = resolve_gate("105-1", "trivial", "review", project_root=project)
|
|
919
|
+
assert result["gate_type"] == "approval"
|
|
920
|
+
assert result["next_agent"] == "sm"
|
|
921
|
+
assert result["next_phase"] == "finish"
|
|
922
|
+
|
|
923
|
+
def test_last_phase_has_no_next(
|
|
924
|
+
self, project: Path, session_with_assessment: Path
|
|
925
|
+
) -> None:
|
|
926
|
+
"""Finish phase (last) should indicate no next phase/agent."""
|
|
927
|
+
result = resolve_gate("105-1", "tdd", "finish", project_root=project)
|
|
928
|
+
# Last phase has no gate and no next — should handle gracefully
|
|
929
|
+
assert result["next_phase"] is None or result.get("error") is not None
|