@pennyfarthing/core 11.0.0-alpha.0 → 11.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -26
- package/package.json +14 -16
- package/packages/core/dist/cli/cyclist-migration.test.js +2 -1
- package/packages/core/dist/cli/cyclist-migration.test.js.map +1 -1
- package/packages/core/dist/cli/ocean-profiles.test.js +5 -4
- package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
- package/packages/core/dist/cli/theme-maker.test.js +5 -4
- package/packages/core/dist/cli/theme-maker.test.js.map +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 +7 -1
- package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/constants.js +2 -0
- 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/public/js/react/react.js +30 -30
- package/packages/core/dist/scripts/generate-report.test.js +2 -2
- package/packages/core/dist/scripts/generate-spider-report.test.js +2 -2
- package/packages/core/dist/scripts/generate-spider.test.js +2 -1
- package/packages/core/dist/scripts/generate-spider.test.js.map +1 -1
- package/packages/core/dist/server/api/file-browser.d.ts.map +1 -1
- package/packages/core/dist/server/api/file-browser.js +19 -1
- package/packages/core/dist/server/api/file-browser.js.map +1 -1
- package/packages/core/dist/server/api/git-fetch-cooldown.test.d.ts +10 -0
- package/packages/core/dist/server/api/git-fetch-cooldown.test.d.ts.map +1 -0
- package/packages/core/dist/server/api/git-fetch-cooldown.test.js +30 -0
- package/packages/core/dist/server/api/git-fetch-cooldown.test.js.map +1 -0
- package/packages/core/dist/server/api/git.d.ts +8 -0
- package/packages/core/dist/server/api/git.d.ts.map +1 -1
- package/packages/core/dist/server/api/git.js +37 -10
- 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/index.d.ts +1 -1
- package/packages/core/dist/server/api/index.d.ts.map +1 -1
- package/packages/core/dist/server/api/index.js +1 -1
- package/packages/core/dist/server/api/index.js.map +1 -1
- package/packages/core/dist/server/api/settings.d.ts.map +1 -1
- package/packages/core/dist/server/api/settings.js +73 -2
- 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/otlp-receiver.d.ts +35 -13
- package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
- package/packages/core/dist/server/otlp-receiver.js +76 -16
- package/packages/core/dist/server/otlp-receiver.js.map +1 -1
- package/packages/core/dist/server/paths.d.ts.map +1 -1
- package/packages/core/dist/server/paths.js +11 -1
- package/packages/core/dist/server/paths.js.map +1 -1
- package/packages/core/dist/server/server.d.ts +3 -1
- package/packages/core/dist/server/server.d.ts.map +1 -1
- package/packages/core/dist/server/server.js +23 -16
- package/packages/core/dist/server/server.js.map +1 -1
- package/packages/core/dist/server/server.test.js.map +1 -1
- 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/variable-resolver.test.js +1 -1
- package/packages/core/dist/workflow/variable-resolver.test.js.map +1 -1
- package/packages/core/dist/workflow/workflow-migration.test.js +4 -3
- package/packages/core/dist/workflow/workflow-migration.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 -30
- package/pennyfarthing-dist/agents/ba.md +30 -29
- package/pennyfarthing-dist/agents/dev.md +76 -41
- package/pennyfarthing-dist/agents/devops.md +57 -21
- package/pennyfarthing-dist/agents/orchestrator.md +3 -11
- 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 -40
- 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 +14 -14
- package/pennyfarthing-dist/guides/agent-coordination.md +7 -7
- package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +6 -6
- 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/agent-session.sh +0 -0
- package/pennyfarthing-dist/scripts/core/check-context.sh +0 -0
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +1 -1
- package/pennyfarthing-dist/scripts/core/prime.sh +0 -0
- package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
- package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +0 -0
- package/pennyfarthing-dist/scripts/git/git-status-all.sh +0 -0
- package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +0 -0
- package/pennyfarthing-dist/scripts/git/release.sh +0 -0
- package/pennyfarthing-dist/scripts/git/worktree-manager.sh +0 -0
- package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/dispatcher-template.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +19 -14
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-push.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
- package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
- package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
- package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
- package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -0
- package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/statusline.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +0 -0
- package/pennyfarthing-dist/scripts/portraits/generate-portraits.py +191 -57
- package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +26 -10
- package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
- package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
- package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
- package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/check.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/complete-step.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +0 -0
- package/pennyfarthing-dist/skills/pf-changelog/SKILL.md +4 -4
- package/pennyfarthing-dist/skills/pf-sprint/skill.md +1 -1
- package/pennyfarthing-dist/skills/pf-story/scripts/create-story.sh +0 -0
- package/pennyfarthing-dist/skills/pf-story/scripts/size-story.sh +0 -0
- package/pennyfarthing-dist/skills/pf-story/scripts/story-template.sh +0 -0
- package/pennyfarthing-dist/skills/pf-systematic-debugging/SKILL.md +0 -1
- package/pennyfarthing-dist/skills/pf-workflow/scripts/list-workflows.sh +0 -0
- package/pennyfarthing-dist/skills/pf-workflow/scripts/resume-workflow.sh +0 -0
- package/pennyfarthing-dist/skills/pf-workflow/scripts/show-workflow.sh +0 -0
- package/pennyfarthing-dist/skills/pf-workflow/scripts/start-workflow.sh +0 -0
- package/pennyfarthing-dist/skills/pf-workflow/scripts/workflow-status.sh +0 -0
- package/pennyfarthing-dist/skills/skill-registry.schema.json +4 -0
- package/pennyfarthing-dist/skills/skill-registry.yaml +8 -21
- 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__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/context.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/session_start_hook.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__/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/changed_panel.py +105 -0
- package/pennyfarthing_scripts/bikerack/debug_panel.py +218 -0
- package/pennyfarthing_scripts/bikerack/diffs_panel.py +203 -27
- package/pennyfarthing_scripts/cli.py +114 -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_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__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/cli.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/prime/__pycache__/persona.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/session_start_hook.py +4 -4
- 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__/loader.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__/yaml_io.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/archive_epic.py +8 -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_cli_normalization.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_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_resolve_gate_file_field.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_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_version_sentinel.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_archive_epic.py +1 -2
- 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__/cli.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/benchmark/package-exports.test.d.ts.map +0 -1
- package/packages/core/dist/benchmark/package-exports.test.js.map +0 -1
- 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/benchmark-integration.test.d.ts +0 -13
- package/packages/core/dist/scripts/benchmark-integration.test.d.ts.map +0 -1
- package/packages/core/dist/scripts/benchmark-integration.test.js +0 -680
- package/packages/core/dist/scripts/benchmark-integration.test.js.map +0 -1
- package/packages/core/dist/scripts/debugging-scenarios.test.d.ts +0 -18
- package/packages/core/dist/scripts/debugging-scenarios.test.d.ts.map +0 -1
- package/packages/core/dist/scripts/debugging-scenarios.test.js +0 -317
- package/packages/core/dist/scripts/debugging-scenarios.test.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/packages/core/dist/scripts/job-fair-aggregator.test.d.ts +0 -14
- package/packages/core/dist/scripts/job-fair-aggregator.test.d.ts.map +0 -1
- package/packages/core/dist/scripts/job-fair-aggregator.test.js +0 -616
- package/packages/core/dist/scripts/job-fair-aggregator.test.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/skills/pf-dev-patterns/SKILL.md +0 -461
- package/scripts/README.md +0 -41
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Complete phase transition with atomic session update.
|
|
2
|
+
|
|
3
|
+
Atomically updates the session file (temp + mv) with phase transition,
|
|
4
|
+
timestamps, and history table entries.
|
|
5
|
+
|
|
6
|
+
Story: 105-1 (Script-First Handoff)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import tempfile
|
|
14
|
+
from datetime import UTC, datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import yaml
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def complete_phase(
|
|
21
|
+
story_id: str,
|
|
22
|
+
workflow: str,
|
|
23
|
+
from_phase: str,
|
|
24
|
+
to_phase: str,
|
|
25
|
+
gate_type: str,
|
|
26
|
+
project_root: Path | None = None,
|
|
27
|
+
) -> dict:
|
|
28
|
+
"""Complete a phase transition with atomic session file update.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
story_id: Story identifier (e.g., "105-1")
|
|
32
|
+
workflow: Workflow name (e.g., "tdd", "trivial")
|
|
33
|
+
from_phase: Phase being completed (e.g., "green")
|
|
34
|
+
to_phase: Phase being entered (e.g., "review")
|
|
35
|
+
gate_type: Gate type that was passed (e.g., "tests_pass")
|
|
36
|
+
project_root: Project root path. Auto-detected if None.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
COMPLETE_RESULT dict with keys:
|
|
40
|
+
status: "success" | "error"
|
|
41
|
+
session_file: str (path to session file)
|
|
42
|
+
error: str | None
|
|
43
|
+
"""
|
|
44
|
+
if project_root is None:
|
|
45
|
+
project_root = _find_project_root()
|
|
46
|
+
|
|
47
|
+
session_path = project_root / ".session" / f"{story_id}-session.md"
|
|
48
|
+
if not session_path.exists():
|
|
49
|
+
return {
|
|
50
|
+
"status": "error",
|
|
51
|
+
"session_file": None,
|
|
52
|
+
"error": "Session file not found",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
content = session_path.read_text()
|
|
56
|
+
now = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
57
|
+
|
|
58
|
+
from_agent = _get_phase_agent(project_root, workflow, from_phase)
|
|
59
|
+
to_agent = _get_phase_agent(project_root, workflow, to_phase)
|
|
60
|
+
|
|
61
|
+
# Update all **Phase:** lines to new phase
|
|
62
|
+
content = re.sub(r"(\*\*Phase:\*\*) \S+", rf"\1 {to_phase}", content)
|
|
63
|
+
|
|
64
|
+
# Update all **Phase Started:** lines to now
|
|
65
|
+
content = re.sub(r"(\*\*Phase Started:\*\*) \S+", rf"\1 {now}", content)
|
|
66
|
+
|
|
67
|
+
# Update Phase History: fill Ended/Duration for from_phase, add new row
|
|
68
|
+
lines = content.splitlines()
|
|
69
|
+
result_lines = []
|
|
70
|
+
for line in lines:
|
|
71
|
+
if line.strip().startswith(f"| {from_phase}"):
|
|
72
|
+
cols = [c.strip() for c in line.split("|") if c.strip()]
|
|
73
|
+
if len(cols) >= 4 and cols[2] == "-":
|
|
74
|
+
started_str = cols[1]
|
|
75
|
+
duration = _calc_duration(started_str, now)
|
|
76
|
+
result_lines.append(
|
|
77
|
+
f"| {from_phase} | {started_str} | {now} | {duration} |"
|
|
78
|
+
)
|
|
79
|
+
result_lines.append(f"| {to_phase} | {now} | - | - |")
|
|
80
|
+
continue
|
|
81
|
+
result_lines.append(line)
|
|
82
|
+
content = "\n".join(result_lines)
|
|
83
|
+
|
|
84
|
+
# Add Handoff History row at end of table
|
|
85
|
+
handoff_row = (
|
|
86
|
+
f"| {from_phase} ({from_agent}) | {to_phase} ({to_agent}) "
|
|
87
|
+
f"| {gate_type} | PASSED | {now} |"
|
|
88
|
+
)
|
|
89
|
+
lines = content.splitlines()
|
|
90
|
+
insert_after = None
|
|
91
|
+
in_handoff = False
|
|
92
|
+
for i, line in enumerate(lines):
|
|
93
|
+
if "### Handoff History" in line:
|
|
94
|
+
in_handoff = True
|
|
95
|
+
if in_handoff and line.strip().startswith("|"):
|
|
96
|
+
insert_after = i
|
|
97
|
+
if insert_after is not None:
|
|
98
|
+
lines.insert(insert_after + 1, handoff_row)
|
|
99
|
+
content = "\n".join(lines)
|
|
100
|
+
|
|
101
|
+
# Atomic write: temp file in same directory + rename
|
|
102
|
+
temp_fd, temp_path_str = tempfile.mkstemp(
|
|
103
|
+
dir=str(session_path.parent), suffix=".tmp"
|
|
104
|
+
)
|
|
105
|
+
os.close(temp_fd)
|
|
106
|
+
temp_path = Path(temp_path_str)
|
|
107
|
+
try:
|
|
108
|
+
temp_path.write_text(content)
|
|
109
|
+
temp_path.rename(session_path)
|
|
110
|
+
except Exception:
|
|
111
|
+
temp_path.unlink(missing_ok=True)
|
|
112
|
+
raise
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"status": "success",
|
|
116
|
+
"session_file": f".session/{story_id}-session.md",
|
|
117
|
+
"error": None,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _calc_duration(started_str: str, ended_str: str) -> str:
|
|
122
|
+
started = datetime.fromisoformat(started_str.replace("Z", "+00:00"))
|
|
123
|
+
ended = datetime.fromisoformat(ended_str.replace("Z", "+00:00"))
|
|
124
|
+
total_seconds = int((ended - started).total_seconds())
|
|
125
|
+
if total_seconds < 60:
|
|
126
|
+
return f"{total_seconds}s"
|
|
127
|
+
minutes = total_seconds // 60
|
|
128
|
+
seconds = total_seconds % 60
|
|
129
|
+
if total_seconds < 3600:
|
|
130
|
+
return f"{minutes}m {seconds}s" if seconds else f"{minutes}m"
|
|
131
|
+
hours = total_seconds // 3600
|
|
132
|
+
rem_minutes = (total_seconds % 3600) // 60
|
|
133
|
+
return f"{hours}h {rem_minutes}m" if rem_minutes else f"{hours}h"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _get_phase_agent(project_root: Path, workflow: str, phase: str) -> str:
|
|
137
|
+
for name in [f"{workflow}.yaml", f"{workflow}/workflow.yaml"]:
|
|
138
|
+
path = project_root / ".pennyfarthing" / "workflows" / name
|
|
139
|
+
if path.exists():
|
|
140
|
+
try:
|
|
141
|
+
data = yaml.safe_load(path.read_text())
|
|
142
|
+
for p in data["workflow"]["phases"]:
|
|
143
|
+
if p["name"] == phase:
|
|
144
|
+
return p.get("agent", phase)
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
147
|
+
return phase
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _find_project_root() -> Path:
|
|
151
|
+
cwd = Path.cwd()
|
|
152
|
+
for parent in [cwd, *cwd.parents]:
|
|
153
|
+
if (parent / ".pennyfarthing").is_dir():
|
|
154
|
+
return parent
|
|
155
|
+
return cwd
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Gate file discovery and resolution.
|
|
2
|
+
|
|
3
|
+
Resolves gate file references (e.g., "gates/tests-pass") to actual file paths.
|
|
4
|
+
Resolution order:
|
|
5
|
+
1. .pennyfarthing/gates/{name}.md (project-local override)
|
|
6
|
+
2. pennyfarthing-dist/gates/{name}.md (built-in fallback)
|
|
7
|
+
|
|
8
|
+
Non-existent files return an error result with status "blocked".
|
|
9
|
+
|
|
10
|
+
Story: 106-4 (Gate File Discovery and Resolution)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def resolve_gate_file(
|
|
19
|
+
gate_ref: str,
|
|
20
|
+
project_root: Path | None = None,
|
|
21
|
+
) -> dict:
|
|
22
|
+
"""Resolve a gate file reference to an absolute file path.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
gate_ref: Gate reference string (e.g., "gates/tests-pass" or "tests-pass")
|
|
26
|
+
project_root: Project root path. Auto-detected if None.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
dict with keys:
|
|
30
|
+
status: "found" | "not_found"
|
|
31
|
+
path: str | None (absolute path if found)
|
|
32
|
+
error: str | None (error message if not found)
|
|
33
|
+
"""
|
|
34
|
+
if project_root is None:
|
|
35
|
+
project_root = _find_project_root()
|
|
36
|
+
|
|
37
|
+
name = _sanitize_gate_name(gate_ref)
|
|
38
|
+
if name is None:
|
|
39
|
+
return _result(
|
|
40
|
+
status="not_found",
|
|
41
|
+
error=f"Invalid gate reference: {gate_ref!r}",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Resolution order: local first, built-in fallback
|
|
45
|
+
search_paths = [
|
|
46
|
+
project_root / ".pennyfarthing" / "gates" / f"{name}.md",
|
|
47
|
+
project_root / "pennyfarthing-dist" / "gates" / f"{name}.md",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
for candidate in search_paths:
|
|
51
|
+
if candidate.is_file():
|
|
52
|
+
return _result(status="found", path=str(candidate.resolve()))
|
|
53
|
+
|
|
54
|
+
return _result(
|
|
55
|
+
status="not_found",
|
|
56
|
+
error=f"Gate file not found: {name}",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _sanitize_gate_name(gate_ref: str) -> str | None:
|
|
61
|
+
"""Extract a clean gate name from a reference string.
|
|
62
|
+
|
|
63
|
+
Strips 'gates/' prefix and '.md' suffix. Rejects empty names
|
|
64
|
+
and path traversal attempts.
|
|
65
|
+
"""
|
|
66
|
+
if not gate_ref:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
name = gate_ref
|
|
70
|
+
# Strip gates/ prefix
|
|
71
|
+
if name.startswith("gates/"):
|
|
72
|
+
name = name[len("gates/"):]
|
|
73
|
+
# Strip .md suffix
|
|
74
|
+
if name.endswith(".md"):
|
|
75
|
+
name = name[: -len(".md")]
|
|
76
|
+
|
|
77
|
+
if not name:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
# Reject path traversal
|
|
81
|
+
if ".." in name or "/" in name:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
return name
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _result(
|
|
88
|
+
status: str,
|
|
89
|
+
path: str | None = None,
|
|
90
|
+
error: str | None = None,
|
|
91
|
+
) -> dict:
|
|
92
|
+
return {
|
|
93
|
+
"status": status,
|
|
94
|
+
"path": path,
|
|
95
|
+
"error": error,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _find_project_root() -> Path:
|
|
100
|
+
"""Walk up from cwd looking for .pennyfarthing/ directory."""
|
|
101
|
+
cwd = Path.cwd()
|
|
102
|
+
for parent in [cwd, *cwd.parents]:
|
|
103
|
+
if (parent / ".pennyfarthing").is_dir():
|
|
104
|
+
return parent
|
|
105
|
+
return cwd
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Gate subagent runner — parse gate files and extract GATE_RESULT.
|
|
2
|
+
|
|
3
|
+
Provides two core functions for the agent exit protocol (step 6):
|
|
4
|
+
1. parse_gate_file(path) — read gate file, extract model/name/content
|
|
5
|
+
2. extract_gate_result(raw_output) — regex-extract GATE_RESULT from subagent output
|
|
6
|
+
|
|
7
|
+
The actual subagent spawning is handled by the Claude agent via Task tool.
|
|
8
|
+
These functions provide the parsing layer around that interaction.
|
|
9
|
+
|
|
10
|
+
Default-deny: missing or unparseable GATE_RESULT always returns fail.
|
|
11
|
+
Gate files are read-only at runtime — never written to.
|
|
12
|
+
|
|
13
|
+
Story: 106-2 (Gate subagent runner with GATE_RESULT contract)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
_DEFAULT_FAIL: dict = {
|
|
22
|
+
"status": "fail",
|
|
23
|
+
"message": "Gate evaluation failed or did not return GATE_RESULT",
|
|
24
|
+
"checks": [],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_gate_file(
|
|
29
|
+
gate_path: str | Path,
|
|
30
|
+
) -> dict:
|
|
31
|
+
"""Parse a gate file and extract its metadata and content.
|
|
32
|
+
|
|
33
|
+
Reads the gate file (read-only), extracts the model attribute and gate name
|
|
34
|
+
from the <gate> tag, and returns the full content for subagent prompting.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
gate_path: Absolute path to the gate file.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
dict with keys:
|
|
41
|
+
status: "ok" | "error"
|
|
42
|
+
name: str | None (gate name from <gate name="...">)
|
|
43
|
+
model: str (model from <gate model="..."> or "haiku")
|
|
44
|
+
content: str | None (full gate file content)
|
|
45
|
+
error: str | None
|
|
46
|
+
"""
|
|
47
|
+
path = Path(gate_path)
|
|
48
|
+
|
|
49
|
+
if not path.exists():
|
|
50
|
+
return {
|
|
51
|
+
"status": "error",
|
|
52
|
+
"name": None,
|
|
53
|
+
"model": "haiku",
|
|
54
|
+
"content": None,
|
|
55
|
+
"error": f"Gate file not found: {path}",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
content = path.read_text()
|
|
59
|
+
|
|
60
|
+
gate_match = re.search(r"<gate\b[^>]*>", content)
|
|
61
|
+
if not gate_match:
|
|
62
|
+
return {
|
|
63
|
+
"status": "error",
|
|
64
|
+
"name": None,
|
|
65
|
+
"model": "haiku",
|
|
66
|
+
"content": None,
|
|
67
|
+
"error": "No <gate> tag found in file",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
gate_tag = gate_match.group(0)
|
|
71
|
+
|
|
72
|
+
name_match = re.search(r'name="([^"]+)"', gate_tag)
|
|
73
|
+
name = name_match.group(1) if name_match else None
|
|
74
|
+
|
|
75
|
+
model_match = re.search(r'model="([^"]+)"', gate_tag)
|
|
76
|
+
model = model_match.group(1) if model_match else "haiku"
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
"status": "ok",
|
|
80
|
+
"name": name,
|
|
81
|
+
"model": model,
|
|
82
|
+
"content": content,
|
|
83
|
+
"error": None,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def extract_gate_result(
|
|
88
|
+
raw_output: str | None,
|
|
89
|
+
) -> dict:
|
|
90
|
+
"""Extract GATE_RESULT from raw subagent output via regex.
|
|
91
|
+
|
|
92
|
+
Parses the subagent's text output to find a GATE_RESULT YAML block.
|
|
93
|
+
Uses regex/grep patterns — NOT a full YAML parser.
|
|
94
|
+
|
|
95
|
+
Default-deny: if GATE_RESULT cannot be extracted, returns fail.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
raw_output: Raw text output from the gate subagent.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
dict with keys:
|
|
102
|
+
status: "pass" | "fail"
|
|
103
|
+
message: str
|
|
104
|
+
checks: list[dict] (each with name, status, detail)
|
|
105
|
+
"""
|
|
106
|
+
if raw_output is None or not raw_output.strip():
|
|
107
|
+
return dict(_DEFAULT_FAIL)
|
|
108
|
+
|
|
109
|
+
# Split on GATE_RESULT: and take the last block (AC7: multiple → last wins)
|
|
110
|
+
blocks = raw_output.split("GATE_RESULT:")
|
|
111
|
+
if len(blocks) < 2:
|
|
112
|
+
return dict(_DEFAULT_FAIL)
|
|
113
|
+
|
|
114
|
+
block = blocks[-1]
|
|
115
|
+
|
|
116
|
+
# Extract status — strict enum: only "pass" or "fail"
|
|
117
|
+
status_match = re.search(r"^\s*status:\s*(pass|fail)\s*$", block, re.MULTILINE)
|
|
118
|
+
if not status_match:
|
|
119
|
+
return dict(_DEFAULT_FAIL)
|
|
120
|
+
|
|
121
|
+
status = status_match.group(1)
|
|
122
|
+
|
|
123
|
+
# Extract message — double-quoted, single-quoted, or unquoted
|
|
124
|
+
message_match = re.search(
|
|
125
|
+
r"""^\s*message:\s*(?:"([^"]*)"|'([^']*)'|(.+?))\s*$""",
|
|
126
|
+
block,
|
|
127
|
+
re.MULTILINE,
|
|
128
|
+
)
|
|
129
|
+
message = ""
|
|
130
|
+
if message_match:
|
|
131
|
+
message = message_match.group(1) or message_match.group(2) or message_match.group(3) or ""
|
|
132
|
+
|
|
133
|
+
# Extract checks list via regex on consecutive name/status/detail lines
|
|
134
|
+
checks: list[dict] = []
|
|
135
|
+
check_pattern = re.compile(
|
|
136
|
+
r"^\s*-\s*name:\s*(?:\"([^\"]*)\"|'([^']*)'|(\S+))\s*\n"
|
|
137
|
+
r"\s*status:\s*(pass|fail)\s*\n"
|
|
138
|
+
r"\s*detail:\s*(?:\"([^\"]*)\"|'([^']*)'|(.+?))\s*$",
|
|
139
|
+
re.MULTILINE,
|
|
140
|
+
)
|
|
141
|
+
for m in check_pattern.finditer(block):
|
|
142
|
+
checks.append({
|
|
143
|
+
"name": m.group(1) or m.group(2) or m.group(3),
|
|
144
|
+
"status": m.group(4),
|
|
145
|
+
"detail": m.group(5) or m.group(6) or m.group(7) or "",
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"status": status,
|
|
150
|
+
"message": message,
|
|
151
|
+
"checks": checks,
|
|
152
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Generate AGENT_COMMAND block for handoff markers.
|
|
2
|
+
|
|
3
|
+
Replaces handoff-marker.sh with a Python implementation that reuses
|
|
4
|
+
the existing context.py module for environment detection.
|
|
5
|
+
|
|
6
|
+
Story: 105-4 (Script-First Handoff)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pennyfarthing_scripts.context import check_context
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def generate_marker(
|
|
15
|
+
next_agent: str | None = None,
|
|
16
|
+
*,
|
|
17
|
+
error: str | None = None,
|
|
18
|
+
) -> str:
|
|
19
|
+
"""Generate an AGENT_COMMAND block for agent handoff.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
next_agent: Agent to hand off to (e.g., "dev", "tea", "reviewer").
|
|
23
|
+
error: If set, generates an error block instead of a handoff.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
YAML-formatted AGENT_COMMAND block string.
|
|
27
|
+
"""
|
|
28
|
+
if error:
|
|
29
|
+
return _block(fallback=error, error=True)
|
|
30
|
+
|
|
31
|
+
if not next_agent:
|
|
32
|
+
return _block(fallback="No next agent specified", error=True)
|
|
33
|
+
|
|
34
|
+
ctx = check_context()
|
|
35
|
+
|
|
36
|
+
cmd = f"/pf-{next_agent}"
|
|
37
|
+
pct = ctx.usable_percent if not ctx.error else "unknown"
|
|
38
|
+
|
|
39
|
+
if pct != "unknown" and pct >= 60:
|
|
40
|
+
context_warning = f" (context: {pct}% - consider /clear before continuing)"
|
|
41
|
+
elif pct != "unknown":
|
|
42
|
+
context_warning = f" (context: {pct}%)"
|
|
43
|
+
else:
|
|
44
|
+
context_warning = ""
|
|
45
|
+
|
|
46
|
+
if not ctx.relay_mode:
|
|
47
|
+
# Relay off — ask for confirmation
|
|
48
|
+
if ctx.is_cyclist:
|
|
49
|
+
return _block(
|
|
50
|
+
marker="<!-- CYCLIST:QUESTION:yesno -->",
|
|
51
|
+
question=f"Ready to hand off to {cmd}?",
|
|
52
|
+
fallback=f"Run `{cmd}` to continue",
|
|
53
|
+
)
|
|
54
|
+
return _block(
|
|
55
|
+
fallback=f"Run `{cmd}` to continue{context_warning}",
|
|
56
|
+
relay_mode=False,
|
|
57
|
+
context_percent=pct,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Relay on — auto-handoff
|
|
61
|
+
# Cyclist uses its feedback loop (QuickActions → slash command injection).
|
|
62
|
+
# Non-Cyclist: we're already in the session, invoke the agent directly.
|
|
63
|
+
marker = None
|
|
64
|
+
if ctx.use_tirepump:
|
|
65
|
+
marker = f"<!-- CYCLIST:CONTEXT_CLEAR:{cmd} -->" if ctx.is_cyclist else None
|
|
66
|
+
else:
|
|
67
|
+
marker = f"<!-- CYCLIST:HANDOFF:{cmd} -->" if ctx.is_cyclist else None
|
|
68
|
+
|
|
69
|
+
if ctx.is_cyclist:
|
|
70
|
+
return _block(
|
|
71
|
+
marker=marker,
|
|
72
|
+
fallback=f"Run `{cmd}` to continue",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Non-Cyclist relay: invoke the next agent directly
|
|
76
|
+
import subprocess
|
|
77
|
+
try:
|
|
78
|
+
result = subprocess.run(
|
|
79
|
+
["pf", "agent", "start", next_agent],
|
|
80
|
+
capture_output=True,
|
|
81
|
+
text=True,
|
|
82
|
+
timeout=30,
|
|
83
|
+
)
|
|
84
|
+
agent_output = result.stdout.strip()
|
|
85
|
+
except Exception as e:
|
|
86
|
+
agent_output = f"Failed to invoke agent: {e}"
|
|
87
|
+
|
|
88
|
+
return _block(
|
|
89
|
+
fallback=f"Run `{cmd}` to continue{context_warning}",
|
|
90
|
+
relay_mode=True,
|
|
91
|
+
context_percent=pct,
|
|
92
|
+
invoke=cmd,
|
|
93
|
+
) + f"\n\n{agent_output}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _block(**fields: object) -> str:
|
|
97
|
+
"""Format an AGENT_COMMAND YAML block."""
|
|
98
|
+
lines = ["---", "AGENT_COMMAND:"]
|
|
99
|
+
for key, value in fields.items():
|
|
100
|
+
if isinstance(value, bool):
|
|
101
|
+
lines.append(f" {key}: {str(value).lower()}")
|
|
102
|
+
elif isinstance(value, str) and value:
|
|
103
|
+
lines.append(f' {key}: "{value}"')
|
|
104
|
+
elif value == "":
|
|
105
|
+
lines.append(f' {key}: ""')
|
|
106
|
+
else:
|
|
107
|
+
lines.append(f" {key}: {value}")
|
|
108
|
+
lines.append("---")
|
|
109
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Resolve gate for current workflow phase.
|
|
2
|
+
|
|
3
|
+
Reads workflow YAML, finds current phase gate, checks for assessment
|
|
4
|
+
section in session file, and returns a structured RESOLVE_RESULT.
|
|
5
|
+
|
|
6
|
+
Story: 105-1 (Script-First Handoff)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def resolve_gate(
|
|
18
|
+
story_id: str,
|
|
19
|
+
workflow: str,
|
|
20
|
+
phase: str,
|
|
21
|
+
project_root: Path | None = None,
|
|
22
|
+
) -> dict:
|
|
23
|
+
"""Resolve the gate for the current workflow phase.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
story_id: Story identifier (e.g., "105-1")
|
|
27
|
+
workflow: Workflow name (e.g., "tdd", "trivial", "patch")
|
|
28
|
+
phase: Current phase name (e.g., "green", "implement", "fix")
|
|
29
|
+
project_root: Project root path. Auto-detected if None.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
RESOLVE_RESULT dict with keys:
|
|
33
|
+
status: "ready" | "blocked" | "skip"
|
|
34
|
+
gate_type: str | None
|
|
35
|
+
gate_file: str | None
|
|
36
|
+
next_agent: str | None
|
|
37
|
+
next_phase: str | None
|
|
38
|
+
assessment_found: bool
|
|
39
|
+
error: str | None
|
|
40
|
+
"""
|
|
41
|
+
if project_root is None:
|
|
42
|
+
project_root = _find_project_root()
|
|
43
|
+
|
|
44
|
+
workflow_path = _find_workflow_yaml(project_root, workflow)
|
|
45
|
+
if workflow_path is None:
|
|
46
|
+
return _result(status="error", error=f"Workflow '{workflow}' not found")
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
data = yaml.safe_load(workflow_path.read_text())
|
|
50
|
+
phases = data["workflow"]["phases"]
|
|
51
|
+
except Exception as e:
|
|
52
|
+
return _result(status="error", error=f"Failed to parse workflow: {e}")
|
|
53
|
+
|
|
54
|
+
current_idx = None
|
|
55
|
+
current_phase = None
|
|
56
|
+
for i, p in enumerate(phases):
|
|
57
|
+
if p["name"] == phase:
|
|
58
|
+
current_idx = i
|
|
59
|
+
current_phase = p
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
if current_phase is None:
|
|
63
|
+
return _result(
|
|
64
|
+
status="error",
|
|
65
|
+
error=f"Phase '{phase}' not found in workflow '{workflow}'",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
gate = current_phase.get("gate")
|
|
69
|
+
|
|
70
|
+
if current_idx + 1 < len(phases):
|
|
71
|
+
nxt = phases[current_idx + 1]
|
|
72
|
+
next_phase = nxt["name"]
|
|
73
|
+
next_agent = nxt["agent"]
|
|
74
|
+
else:
|
|
75
|
+
next_phase = None
|
|
76
|
+
next_agent = None
|
|
77
|
+
|
|
78
|
+
if not gate:
|
|
79
|
+
return _result(
|
|
80
|
+
status="skip",
|
|
81
|
+
next_agent=next_agent,
|
|
82
|
+
next_phase=next_phase,
|
|
83
|
+
assessment_found=True,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
gate_type = gate.get("type")
|
|
87
|
+
gate_file = gate.get("file")
|
|
88
|
+
|
|
89
|
+
if gate_type == "manual":
|
|
90
|
+
return _result(
|
|
91
|
+
status="skip",
|
|
92
|
+
gate_type="manual",
|
|
93
|
+
next_agent=next_agent,
|
|
94
|
+
next_phase=next_phase,
|
|
95
|
+
assessment_found=True,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
session_path = project_root / ".session" / f"{story_id}-session.md"
|
|
99
|
+
assessment_found = False
|
|
100
|
+
if session_path.exists():
|
|
101
|
+
content = session_path.read_text()
|
|
102
|
+
assessment_found = bool(
|
|
103
|
+
re.search(r"^##\s+.*Assessment", content, re.MULTILINE)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
status = "ready" if assessment_found else "blocked"
|
|
107
|
+
return _result(
|
|
108
|
+
status=status,
|
|
109
|
+
gate_type=gate_type,
|
|
110
|
+
gate_file=gate_file,
|
|
111
|
+
next_agent=next_agent,
|
|
112
|
+
next_phase=next_phase,
|
|
113
|
+
assessment_found=assessment_found,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _result(
|
|
118
|
+
status: str,
|
|
119
|
+
gate_type: str | None = None,
|
|
120
|
+
gate_file: str | None = None,
|
|
121
|
+
next_agent: str | None = None,
|
|
122
|
+
next_phase: str | None = None,
|
|
123
|
+
assessment_found: bool = False,
|
|
124
|
+
error: str | None = None,
|
|
125
|
+
) -> dict:
|
|
126
|
+
return {
|
|
127
|
+
"status": status,
|
|
128
|
+
"gate_type": gate_type,
|
|
129
|
+
"gate_file": gate_file,
|
|
130
|
+
"next_agent": next_agent,
|
|
131
|
+
"next_phase": next_phase,
|
|
132
|
+
"assessment_found": assessment_found,
|
|
133
|
+
"error": error,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _find_workflow_yaml(project_root: Path, workflow: str) -> Path | None:
|
|
138
|
+
flat = project_root / ".pennyfarthing" / "workflows" / f"{workflow}.yaml"
|
|
139
|
+
if flat.exists():
|
|
140
|
+
return flat
|
|
141
|
+
subdir = project_root / ".pennyfarthing" / "workflows" / workflow / "workflow.yaml"
|
|
142
|
+
if subdir.exists():
|
|
143
|
+
return subdir
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _find_project_root() -> Path:
|
|
148
|
+
cwd = Path.cwd()
|
|
149
|
+
for parent in [cwd, *cwd.parents]:
|
|
150
|
+
if (parent / ".pennyfarthing").is_dir():
|
|
151
|
+
return parent
|
|
152
|
+
return cwd
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|