@pennyfarthing/core 10.0.3 → 10.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 +9 -7
- package/package.json +7 -1
- package/packages/core/dist/cli/commands/cyclist.d.ts +5 -1
- package/packages/core/dist/cli/commands/cyclist.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/cyclist.js +4 -4
- package/packages/core/dist/cli/commands/cyclist.js.map +1 -1
- package/packages/core/dist/cli/commands/cyclist.test.js +2 -2
- package/packages/core/dist/cli/commands/cyclist.test.js.map +1 -1
- package/packages/core/dist/cli/commands/doctor-legacy.test.js +17 -16
- package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -1
- package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/doctor.js +251 -4
- package/packages/core/dist/cli/commands/doctor.js.map +1 -1
- package/packages/core/dist/cli/commands/init.d.ts +7 -0
- package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/init.js +43 -7
- package/packages/core/dist/cli/commands/init.js.map +1 -1
- package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/update.js +26 -0
- package/packages/core/dist/cli/commands/update.js.map +1 -1
- package/packages/core/dist/cli/index.js +1 -1
- package/packages/core/dist/cli/index.js.map +1 -1
- package/packages/core/dist/cli/ocean-profiles.test.js +1 -1
- package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
- package/packages/core/dist/cli/utils/files.d.ts +10 -0
- package/packages/core/dist/cli/utils/files.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/files.js +35 -0
- package/packages/core/dist/cli/utils/files.js.map +1 -1
- package/packages/core/dist/cli/utils/python.d.ts +22 -0
- package/packages/core/dist/cli/utils/python.d.ts.map +1 -0
- package/packages/core/dist/cli/utils/python.js +102 -0
- package/packages/core/dist/cli/utils/python.js.map +1 -0
- package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/settings.js +10 -0
- package/packages/core/dist/cli/utils/settings.js.map +1 -1
- package/packages/core/dist/scripts/generate-report.d.ts.map +1 -1
- package/packages/core/dist/scripts/generate-report.js +11 -7
- package/packages/core/dist/scripts/generate-report.js.map +1 -1
- package/packages/core/dist/scripts/generate-spider-report.d.ts.map +1 -1
- package/packages/core/dist/scripts/generate-spider-report.js +12 -8
- package/packages/core/dist/scripts/generate-spider-report.js.map +1 -1
- package/packages/core/dist/scripts/generate-spider.d.ts.map +1 -1
- package/packages/core/dist/scripts/generate-spider.js +6 -4
- package/packages/core/dist/scripts/generate-spider.js.map +1 -1
- package/packages/core/dist/scripts/generate-spider.test.js +2 -2
- package/packages/core/dist/scripts/generate-spider.test.js.map +1 -1
- package/pennyfarthing-dist/agents/README.md +1 -3
- package/pennyfarthing-dist/agents/architect.md +0 -6
- package/pennyfarthing-dist/agents/devops.md +0 -6
- package/pennyfarthing-dist/agents/orchestrator.md +0 -6
- package/pennyfarthing-dist/agents/pm.md +1 -7
- package/pennyfarthing-dist/agents/sm-finish.md +1 -1
- package/pennyfarthing-dist/agents/sm-setup.md +2 -2
- package/pennyfarthing-dist/agents/sm.md +4 -11
- package/pennyfarthing-dist/commands/architect.md +11 -3
- package/pennyfarthing-dist/commands/close-epic.md +24 -131
- package/pennyfarthing-dist/commands/create-theme.md +14 -24
- package/pennyfarthing-dist/commands/dev.md +11 -3
- package/pennyfarthing-dist/commands/devops.md +11 -3
- package/pennyfarthing-dist/commands/health-check.md +1 -3
- package/pennyfarthing-dist/commands/help.md +8 -12
- package/pennyfarthing-dist/commands/list-themes.md +14 -16
- package/pennyfarthing-dist/commands/orchestrator.md +11 -3
- package/pennyfarthing-dist/commands/parallel-work.md +1 -3
- package/pennyfarthing-dist/commands/pm.md +11 -3
- package/pennyfarthing-dist/commands/prime.md +6 -6
- package/pennyfarthing-dist/commands/repo-status.md +2 -2
- package/pennyfarthing-dist/commands/reviewer.md +11 -3
- package/pennyfarthing-dist/commands/run-ci.md +1 -1
- package/pennyfarthing-dist/commands/set-theme.md +14 -51
- package/pennyfarthing-dist/commands/setup.md +1 -1
- package/pennyfarthing-dist/commands/show-theme.md +14 -16
- package/pennyfarthing-dist/commands/sm.md +11 -3
- package/pennyfarthing-dist/commands/sprint.md +8 -8
- package/pennyfarthing-dist/commands/tea.md +11 -3
- package/pennyfarthing-dist/commands/tech-writer.md +11 -3
- package/pennyfarthing-dist/commands/theme-maker.md +14 -671
- package/pennyfarthing-dist/commands/theme.md +95 -0
- package/pennyfarthing-dist/commands/ux-designer.md +11 -3
- package/pennyfarthing-dist/commands/work.md +3 -5
- package/pennyfarthing-dist/guides/agent-coordination.md +11 -13
- package/pennyfarthing-dist/guides/agent-template-tactical.md +2 -3
- package/pennyfarthing-dist/guides/command-tag-taxonomy.md +212 -0
- package/pennyfarthing-dist/guides/hooks.md +5 -5
- package/pennyfarthing-dist/guides/patterns/fan-out-fan-in-pattern.md +3 -3
- package/pennyfarthing-dist/guides/patterns/helper-delegation-pattern.md +9 -59
- package/pennyfarthing-dist/guides/patterns/tdd-flow-pattern.md +4 -5
- package/pennyfarthing-dist/guides/prime.md +2 -2
- package/pennyfarthing-dist/guides/skill-schema.md +25 -26
- package/pennyfarthing-dist/guides/xml-tags.md +2 -2
- package/pennyfarthing-dist/scripts/README.md +2 -2
- package/pennyfarthing-dist/scripts/core/agent-session.sh +6 -2
- package/pennyfarthing-dist/scripts/core/prime.sh +8 -10
- package/pennyfarthing-dist/scripts/git/git-status-all.sh +1 -1
- package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +8 -6
- package/pennyfarthing-dist/scripts/git/worktree-manager.sh +3 -3
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +14 -12
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +4 -3
- package/pennyfarthing-dist/scripts/hooks/pre-push.sh +11 -5
- package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +1 -1
- package/pennyfarthing-dist/scripts/misc/README.md +1 -1
- package/pennyfarthing-dist/scripts/misc/repo-utils.sh +3 -3
- package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +1 -2
- package/pennyfarthing-dist/scripts/sprint/README.md +32 -17
- package/pennyfarthing-dist/scripts/story/README.md +1 -1
- package/pennyfarthing-dist/scripts/test/test-setup.sh +1 -1
- package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +5 -5
- package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +3 -79
- package/pennyfarthing-dist/scripts/theme/README.md +1 -1
- package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -1
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +62 -17
- package/pennyfarthing-dist/skills/dev-patterns/SKILL.md +2 -2
- package/pennyfarthing-dist/skills/skill-registry.yaml +41 -28
- package/pennyfarthing-dist/skills/sprint/skill.md +386 -68
- package/pennyfarthing-dist/skills/story/skill.md +14 -206
- package/pennyfarthing-dist/skills/theme/skill.md +290 -75
- package/pennyfarthing-dist/skills/theme-creation/SKILL.md +23 -166
- package/pennyfarthing-dist/skills/workflow/skill.md +4 -4
- package/pennyfarthing-dist/templates/agent-scopes.yaml.template +0 -11
- package/pennyfarthing-dist/templates/auto-load-sm.sh.template +14 -0
- package/pennyfarthing-dist/templates/settings.local.json.template +9 -0
- package/pennyfarthing-dist/workflows/2party-tdd.yaml +399 -0
- package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +42 -25
- package/pennyfarthing-dist/workflows/git-cleanup.yaml +1 -1
- package/pennyfarthing-dist/workflows/project-setup/steps/step-10-complete.md +1 -1
- package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/cli.py +15 -0
- package/pennyfarthing_scripts/codemarkers/__init__.py +19 -0
- package/pennyfarthing_scripts/codemarkers/__main__.py +6 -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/codemarkers/analyze.py +326 -0
- package/pennyfarthing_scripts/codemarkers/cli.py +129 -0
- package/pennyfarthing_scripts/codemarkers/formatters.py +89 -0
- package/pennyfarthing_scripts/codemarkers/models.py +45 -0
- package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/config.py +2 -1
- package/pennyfarthing_scripts/complexity/__init__.py +15 -0
- package/pennyfarthing_scripts/complexity/__main__.py +6 -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/complexity/analyze.py +207 -0
- package/pennyfarthing_scripts/complexity/cli.py +78 -0
- package/pennyfarthing_scripts/complexity/formatters.py +64 -0
- package/pennyfarthing_scripts/complexity/models.py +32 -0
- package/pennyfarthing_scripts/deadcode/__init__.py +6 -0
- package/pennyfarthing_scripts/deadcode/__main__.py +6 -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/deadcode/analyze.py +323 -0
- package/pennyfarthing_scripts/deadcode/cli.py +163 -0
- package/pennyfarthing_scripts/deadcode/formatters.py +106 -0
- package/pennyfarthing_scripts/deadcode/models.py +54 -0
- package/pennyfarthing_scripts/dependencies/__init__.py +20 -0
- package/pennyfarthing_scripts/dependencies/__main__.py +5 -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/dependencies/analyze.py +155 -0
- package/pennyfarthing_scripts/dependencies/cli.py +72 -0
- package/pennyfarthing_scripts/dependencies/formatters.py +63 -0
- package/pennyfarthing_scripts/dependencies/models.py +39 -0
- package/pennyfarthing_scripts/healthscore/__init__.py +21 -0
- package/pennyfarthing_scripts/healthscore/__main__.py +6 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/analyze.py +161 -0
- package/pennyfarthing_scripts/healthscore/cli.py +76 -0
- package/pennyfarthing_scripts/healthscore/formatters.py +46 -0
- package/pennyfarthing_scripts/healthscore/models.py +44 -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/hotspots/analyze.py +28 -1
- package/pennyfarthing_scripts/hotspots/cli.py +11 -9
- package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/bidirectional.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__/operations.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/bidirectional.py +42 -15
- package/pennyfarthing_scripts/jira/cli.py +78 -1
- package/pennyfarthing_scripts/jira/client.py +28 -0
- package/pennyfarthing_scripts/prime/__pycache__/cli.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__/tiers.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/workflow.py +5 -3
- 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__/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__/validator.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/archive.py +63 -6
- package/pennyfarthing_scripts/sprint/archive_epic.py +198 -85
- package/pennyfarthing_scripts/sprint/cli.py +1565 -65
- package/pennyfarthing_scripts/sprint/epic_add.py +173 -0
- package/pennyfarthing_scripts/sprint/loader.py +46 -2
- package/pennyfarthing_scripts/sprint/story_add.py +202 -27
- package/pennyfarthing_scripts/sprint/story_finish.py +211 -0
- package/pennyfarthing_scripts/sprint/validate_cmd.py +44 -5
- package/pennyfarthing_scripts/sprint/validator.py +13 -3
- package/pennyfarthing_scripts/sprint/work.py +43 -3
- package/pennyfarthing_scripts/sprint/yaml_io.py +124 -15
- package/pennyfarthing_scripts/tests/__pycache__/test_codemarkers.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_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_update.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_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/test_codemarkers.py +682 -0
- package/pennyfarthing_scripts/tests/test_healthscore.py +524 -0
- package/pennyfarthing_scripts/tests/test_sprint_package.py +166 -0
- package/pennyfarthing_scripts/tests/test_yaml_io.py +117 -0
- package/pennyfarthing_scripts/theme/__init__.py +5 -0
- package/pennyfarthing_scripts/theme/__main__.py +6 -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/theme/cli.py +286 -0
- package/scripts/README.md +53 -0
- package/scripts/postinstall.cjs +34 -0
- package/pennyfarthing-dist/agents/workflow-status-check.md +0 -96
- package/pennyfarthing-dist/scripts/sprint/archive-story.sh +0 -133
- package/pennyfarthing-dist/scripts/sprint/available-stories.sh +0 -91
- package/pennyfarthing-dist/scripts/sprint/check-story.sh +0 -158
- package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +0 -52
- package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +0 -63
- package/pennyfarthing-dist/scripts/sprint/list-future.sh +0 -145
- package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +0 -110
- package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +0 -148
- package/pennyfarthing-dist/scripts/sprint/sprint-common.sh +0 -415
- package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +0 -33
- package/pennyfarthing-dist/scripts/sprint/sprint-metrics.sh +0 -230
- package/pennyfarthing-dist/scripts/sprint/sprint-status.sh +0 -134
- package/pennyfarthing-dist/scripts/sprint/validate-sprint-yaml.sh +0 -139
- package/pennyfarthing-dist/skills/sprint/scripts/archive-story.sh +0 -101
- package/pennyfarthing-dist/skills/sprint/scripts/available-stories.sh +0 -97
- package/pennyfarthing-dist/skills/sprint/scripts/check-story.sh +0 -164
- package/pennyfarthing-dist/skills/sprint/scripts/create-jira-epic.sh +0 -23
- package/pennyfarthing-dist/skills/sprint/scripts/new-sprint.sh +0 -116
- package/pennyfarthing-dist/skills/sprint/scripts/promote-epic.sh +0 -164
- package/pennyfarthing-dist/skills/sprint/scripts/sprint-info.sh +0 -39
- package/pennyfarthing-dist/skills/sprint/scripts/sprint-status.sh +0 -147
- package/pennyfarthing-dist/skills/sprint/scripts/sync-epic-jira.sh +0 -23
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Finish a completed story: archive, merge PR, update Jira, update YAML.
|
|
2
|
+
|
|
3
|
+
Replaces finish-story.sh with native Python that correctly handles
|
|
4
|
+
sharded epic YAML files via read_sprint/write_sprint.
|
|
5
|
+
|
|
6
|
+
Steps:
|
|
7
|
+
1. Archive session file to sprint/archive/{jira-key}-session.md
|
|
8
|
+
2. Squash merge PR via gh (handle already-merged)
|
|
9
|
+
3. Transition Jira to Done
|
|
10
|
+
4. Update sprint YAML (status: done, completed date, remove assigned_to)
|
|
11
|
+
5. Archive completed epics
|
|
12
|
+
6. Git cleanup (checkout develop, pull, delete local branch)
|
|
13
|
+
7. Remove session file
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
from datetime import date
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from pennyfarthing_scripts.sprint.loader import find_epic, find_story
|
|
24
|
+
from pennyfarthing_scripts.sprint.yaml_io import read_sprint, write_sprint
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
SESSION_FIELD_RE = re.compile(r"\*\*(\w[\w\s]*):\*\*\s*(.*)")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _parse_session(session_path: Path) -> dict[str, str]:
|
|
31
|
+
"""Extract metadata fields from a session markdown file.
|
|
32
|
+
|
|
33
|
+
Parses lines like ``**Jira:** MSSCI-14467`` and
|
|
34
|
+
``**PR:** #748 - title`` into a dict.
|
|
35
|
+
"""
|
|
36
|
+
fields: dict[str, str] = {}
|
|
37
|
+
if not session_path.exists():
|
|
38
|
+
return fields
|
|
39
|
+
for line in session_path.read_text().splitlines():
|
|
40
|
+
m = SESSION_FIELD_RE.search(line)
|
|
41
|
+
if m:
|
|
42
|
+
key = m.group(1).strip().lower()
|
|
43
|
+
value = m.group(2).strip()
|
|
44
|
+
fields[key] = value
|
|
45
|
+
return fields
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _extract_jira_key(fields: dict[str, str]) -> str | None:
|
|
49
|
+
"""Get Jira key from session fields, handling markdown link format."""
|
|
50
|
+
raw = fields.get("jira", "")
|
|
51
|
+
# Strip markdown link: [MSSCI-14467](https://...)
|
|
52
|
+
raw = re.sub(r"\[([^\]]+)\].*", r"\1", raw).strip()
|
|
53
|
+
if re.match(r"^MSSCI-\d+$", raw):
|
|
54
|
+
return raw
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _extract_pr_number(fields: dict[str, str]) -> str | None:
|
|
59
|
+
"""Get PR number from session fields like ``#748 - title``."""
|
|
60
|
+
raw = fields.get("pr", "")
|
|
61
|
+
m = re.search(r"#(\d+)", raw)
|
|
62
|
+
return m.group(1) if m else None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _extract_branch(fields: dict[str, str]) -> str | None:
|
|
66
|
+
"""Get branch name, stripping trailing annotations like ``(pushed)``."""
|
|
67
|
+
raw = fields.get("branch", "")
|
|
68
|
+
return re.sub(r"\s*\(.*\)\s*$", "", raw).strip() or None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _run(cmd: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
|
|
72
|
+
"""Run a subprocess with sane defaults."""
|
|
73
|
+
return subprocess.run(cmd, capture_output=True, text=True, **kwargs)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def finish_story(
|
|
77
|
+
project_root: Path,
|
|
78
|
+
story_id: str,
|
|
79
|
+
*,
|
|
80
|
+
dry_run: bool = False,
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
"""Finish a story: archive, merge, update Jira, update YAML, clean up.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
project_root: Project root directory.
|
|
86
|
+
story_id: Story ID (e.g., "83-2").
|
|
87
|
+
dry_run: If True, report what would happen without side-effects.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Result dict ``{success, data?, error?, steps?}``.
|
|
91
|
+
"""
|
|
92
|
+
session_path = project_root / ".session" / f"{story_id}-session.md"
|
|
93
|
+
sprint_path = project_root / "sprint" / "current-sprint.yaml"
|
|
94
|
+
archive_dir = project_root / "sprint" / "archive"
|
|
95
|
+
archive_dir.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
|
|
97
|
+
# --- Validate session ---
|
|
98
|
+
if not session_path.exists():
|
|
99
|
+
return {"success": False, "error": f"Session file not found: {session_path}"}
|
|
100
|
+
|
|
101
|
+
fields = _parse_session(session_path)
|
|
102
|
+
jira_key = _extract_jira_key(fields)
|
|
103
|
+
branch = _extract_branch(fields)
|
|
104
|
+
pr_number = _extract_pr_number(fields)
|
|
105
|
+
|
|
106
|
+
# Fallback: resolve Jira key from sprint YAML
|
|
107
|
+
if not jira_key:
|
|
108
|
+
try:
|
|
109
|
+
data = read_sprint(sprint_path)
|
|
110
|
+
parts = story_id.split("-")
|
|
111
|
+
if len(parts) >= 2:
|
|
112
|
+
epic = find_epic(data, parts[0])
|
|
113
|
+
story = find_story(epic, story_id) if epic else None
|
|
114
|
+
if story:
|
|
115
|
+
jira_key = story.get("jira")
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
if not jira_key:
|
|
120
|
+
return {"success": False, "error": f"Could not determine Jira key for {story_id}"}
|
|
121
|
+
|
|
122
|
+
# Fallback: resolve PR from GitHub if not in session
|
|
123
|
+
if not pr_number and branch:
|
|
124
|
+
result = _run(["gh", "pr", "list", "--head", branch, "--json", "number", "--jq", ".[0].number"])
|
|
125
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
126
|
+
pr_number = result.stdout.strip()
|
|
127
|
+
|
|
128
|
+
today = date.today().isoformat()
|
|
129
|
+
steps: list[dict[str, Any]] = []
|
|
130
|
+
|
|
131
|
+
if dry_run:
|
|
132
|
+
steps.append({"step": 1, "action": f"Archive session → {archive_dir / f'{jira_key}-session.md'}"})
|
|
133
|
+
if pr_number:
|
|
134
|
+
steps.append({"step": 2, "action": f"Merge PR #{pr_number} (squash, delete branch)"})
|
|
135
|
+
else:
|
|
136
|
+
steps.append({"step": 2, "action": "No PR to merge"})
|
|
137
|
+
steps.append({"step": 3, "action": f"Transition {jira_key} to Done"})
|
|
138
|
+
steps.append({"step": 4, "action": f"Update sprint YAML (status: done, completed: {today})"})
|
|
139
|
+
steps.append({"step": 5, "action": "Archive completed epics"})
|
|
140
|
+
steps.append({"step": 6, "action": f"Delete local branch: {branch}"})
|
|
141
|
+
steps.append({"step": 7, "action": "Remove session file"})
|
|
142
|
+
return {"success": True, "dry_run": True, "jira_key": jira_key, "steps": steps}
|
|
143
|
+
|
|
144
|
+
# --- Step 1: Archive session ---
|
|
145
|
+
archive_dest = archive_dir / f"{jira_key}-session.md"
|
|
146
|
+
shutil.copy2(session_path, archive_dest)
|
|
147
|
+
steps.append({"step": 1, "action": "archive_session", "dest": str(archive_dest)})
|
|
148
|
+
|
|
149
|
+
# --- Step 2: Merge PR ---
|
|
150
|
+
if pr_number:
|
|
151
|
+
result = _run(["gh", "pr", "merge", pr_number, "--squash", "--delete-branch"])
|
|
152
|
+
if result.returncode == 0:
|
|
153
|
+
steps.append({"step": 2, "action": "merge_pr", "pr": pr_number})
|
|
154
|
+
else:
|
|
155
|
+
steps.append({"step": 2, "action": "merge_pr", "pr": pr_number, "warning": "Already merged or failed"})
|
|
156
|
+
else:
|
|
157
|
+
steps.append({"step": 2, "action": "merge_pr", "skipped": True})
|
|
158
|
+
|
|
159
|
+
# --- Step 3: Transition Jira ---
|
|
160
|
+
result = _run(["jira", "issue", "move", jira_key, "Done"])
|
|
161
|
+
if result.returncode == 0:
|
|
162
|
+
steps.append({"step": 3, "action": "jira_done", "key": jira_key})
|
|
163
|
+
else:
|
|
164
|
+
steps.append({"step": 3, "action": "jira_done", "key": jira_key, "warning": "Already Done or failed"})
|
|
165
|
+
|
|
166
|
+
# --- Step 4: Update sprint YAML ---
|
|
167
|
+
try:
|
|
168
|
+
data = read_sprint(sprint_path)
|
|
169
|
+
parts = story_id.split("-")
|
|
170
|
+
if len(parts) < 2:
|
|
171
|
+
steps.append({"step": 4, "action": "yaml_update", "error": f"Invalid story ID: {story_id}"})
|
|
172
|
+
else:
|
|
173
|
+
epic = find_epic(data, parts[0])
|
|
174
|
+
story = find_story(epic, story_id) if epic else None
|
|
175
|
+
if story:
|
|
176
|
+
story["status"] = "done"
|
|
177
|
+
story["completed"] = today
|
|
178
|
+
if "assigned_to" in story:
|
|
179
|
+
del story["assigned_to"]
|
|
180
|
+
write_sprint(sprint_path, data)
|
|
181
|
+
steps.append({"step": 4, "action": "yaml_update", "status": "done", "completed": today})
|
|
182
|
+
else:
|
|
183
|
+
steps.append({"step": 4, "action": "yaml_update", "warning": f"Story {story_id} not found in YAML"})
|
|
184
|
+
except Exception as exc:
|
|
185
|
+
steps.append({"step": 4, "action": "yaml_update", "error": str(exc)})
|
|
186
|
+
|
|
187
|
+
# --- Step 5: Archive completed epics ---
|
|
188
|
+
result = _run(
|
|
189
|
+
["python", "-m", "pennyfarthing_scripts.cli", "sprint", "epic", "archive"],
|
|
190
|
+
cwd=str(project_root),
|
|
191
|
+
)
|
|
192
|
+
steps.append({"step": 5, "action": "archive_epics", "ran": True})
|
|
193
|
+
|
|
194
|
+
# --- Step 6: Git cleanup ---
|
|
195
|
+
_run(["git", "checkout", "develop"], cwd=str(project_root))
|
|
196
|
+
_run(["git", "pull", "origin", "develop"], cwd=str(project_root))
|
|
197
|
+
if branch:
|
|
198
|
+
_run(["git", "branch", "-d", branch], cwd=str(project_root))
|
|
199
|
+
steps.append({"step": 6, "action": "git_cleanup", "branch": branch})
|
|
200
|
+
|
|
201
|
+
# --- Step 7: Remove session file ---
|
|
202
|
+
if session_path.exists():
|
|
203
|
+
session_path.unlink()
|
|
204
|
+
steps.append({"step": 7, "action": "remove_session"})
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
"success": True,
|
|
208
|
+
"story_id": story_id,
|
|
209
|
+
"jira_key": jira_key,
|
|
210
|
+
"steps": steps,
|
|
211
|
+
}
|
|
@@ -15,7 +15,14 @@ from pathlib import Path
|
|
|
15
15
|
import click
|
|
16
16
|
import yaml
|
|
17
17
|
|
|
18
|
-
from pennyfarthing_scripts.sprint.validator import
|
|
18
|
+
from pennyfarthing_scripts.sprint.validator import (
|
|
19
|
+
REQUIRED_INITIATIVE_FIELDS,
|
|
20
|
+
VALID_INITIATIVE_STATUSES,
|
|
21
|
+
ValidationResult,
|
|
22
|
+
validate_epic,
|
|
23
|
+
validate_full_sprint,
|
|
24
|
+
validate_future,
|
|
25
|
+
)
|
|
19
26
|
from pennyfarthing_scripts.sprint.yaml_io import (
|
|
20
27
|
EPIC_KEY_ORDER,
|
|
21
28
|
SPRINT_KEY_ORDER,
|
|
@@ -142,6 +149,25 @@ def check_format_drift(path: Path) -> list[FormatIssue]:
|
|
|
142
149
|
return issues
|
|
143
150
|
|
|
144
151
|
|
|
152
|
+
def _validate_initiative_shard(data: dict) -> ValidationResult:
|
|
153
|
+
"""Validate a standalone initiative shard file (initiative-*.yaml)."""
|
|
154
|
+
result = ValidationResult(valid=True)
|
|
155
|
+
if not isinstance(data, dict):
|
|
156
|
+
result.add_error("Initiative shard must be a mapping", "")
|
|
157
|
+
return result
|
|
158
|
+
for field_name in REQUIRED_INITIATIVE_FIELDS:
|
|
159
|
+
if field_name not in data:
|
|
160
|
+
result.add_error(f"Missing required field: {field_name}", field_name)
|
|
161
|
+
if "status" in data:
|
|
162
|
+
status = data["status"]
|
|
163
|
+
if status and status not in VALID_INITIATIVE_STATUSES:
|
|
164
|
+
result.add_error(
|
|
165
|
+
f"Invalid initiative status '{status}'. Must be one of: {', '.join(sorted(VALID_INITIATIVE_STATUSES))}",
|
|
166
|
+
"status",
|
|
167
|
+
)
|
|
168
|
+
return result
|
|
169
|
+
|
|
170
|
+
|
|
145
171
|
def validate_sprint_yaml(path: Path, fix: bool = False) -> ValidateResult:
|
|
146
172
|
"""Validate a sprint YAML file for syntax, schema, and format issues.
|
|
147
173
|
|
|
@@ -204,8 +230,21 @@ def validate_sprint_yaml(path: Path, fix: bool = False) -> ValidateResult:
|
|
|
204
230
|
return result
|
|
205
231
|
|
|
206
232
|
# Step 2: Schema validation — detect file type and use appropriate validator
|
|
233
|
+
#
|
|
234
|
+
# Shard files (epic-*.yaml, initiative-*.yaml) are standalone fragments
|
|
235
|
+
# that don't have the full sprint/future structure. Validate their
|
|
236
|
+
# internal structure only — the index files handle cross-references.
|
|
237
|
+
is_epic_shard = path.name.startswith("epic-") and path.name.endswith(".yaml")
|
|
238
|
+
is_initiative_shard = path.name.startswith("initiative-") and path.name.endswith(".yaml")
|
|
207
239
|
is_future = path.name == "future.yaml" or "future" in data
|
|
208
|
-
|
|
240
|
+
|
|
241
|
+
if is_epic_shard:
|
|
242
|
+
# Epic shard: validate as a single epic (has id, title, stories)
|
|
243
|
+
schema_result = validate_epic(data, set(), 0)
|
|
244
|
+
elif is_initiative_shard:
|
|
245
|
+
# Initiative shard: validate as a single initiative (has name, status)
|
|
246
|
+
schema_result = _validate_initiative_shard(data)
|
|
247
|
+
elif is_future:
|
|
209
248
|
schema_result = validate_future(data)
|
|
210
249
|
else:
|
|
211
250
|
schema_result = validate_full_sprint(data)
|
|
@@ -218,13 +257,13 @@ def validate_sprint_yaml(path: Path, fix: bool = False) -> ValidateResult:
|
|
|
218
257
|
category="schema",
|
|
219
258
|
))
|
|
220
259
|
|
|
221
|
-
# Step 3: Format drift detection (sprint files only — future.yaml
|
|
222
|
-
if not is_future:
|
|
260
|
+
# Step 3: Format drift detection (sprint files only — shards and future.yaml have different structure)
|
|
261
|
+
if not is_future and not is_epic_shard and not is_initiative_shard:
|
|
223
262
|
format_issues = check_format_drift(path)
|
|
224
263
|
result.format_issues = format_issues
|
|
225
264
|
|
|
226
265
|
# Step 4: Fix if requested (only format issues, not schema; sprint files only)
|
|
227
|
-
if fix and not is_future and path.exists():
|
|
266
|
+
if fix and not is_future and not is_epic_shard and not is_initiative_shard and path.exists():
|
|
228
267
|
try:
|
|
229
268
|
canon_data = read_sprint(path)
|
|
230
269
|
write_sprint(path, canon_data)
|
|
@@ -65,7 +65,7 @@ class ValidationResult:
|
|
|
65
65
|
# =============================================================================
|
|
66
66
|
|
|
67
67
|
VALID_SPRINT_STATUSES = {"active", "closed"}
|
|
68
|
-
VALID_STORY_STATUSES = {"backlog", "ready", "in_progress", "done", "canceled"}
|
|
68
|
+
VALID_STORY_STATUSES = {"backlog", "ready", "in_progress", "done", "canceled", "planning"}
|
|
69
69
|
JIRA_KEY_PATTERN = re.compile(r"^MSSCI-\d{5}$")
|
|
70
70
|
ISO_DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
71
71
|
|
|
@@ -87,7 +87,7 @@ REQUIRED_FUTURE_EPIC_FIELDS = {"id", "title", "points"}
|
|
|
87
87
|
# Required fields for future.yaml story (what promote-epic.sh transforms)
|
|
88
88
|
REQUIRED_FUTURE_STORY_FIELDS = {"id", "title", "points"}
|
|
89
89
|
|
|
90
|
-
VALID_INITIATIVE_STATUSES = {"ready", "planning", "blocked", "research_complete", "backlog", "complete"}
|
|
90
|
+
VALID_INITIATIVE_STATUSES = {"ready", "planning", "blocked", "research_complete", "backlog", "complete", "canceled"}
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
# =============================================================================
|
|
@@ -282,10 +282,13 @@ def validate_full_sprint(data: dict[str, Any]) -> ValidationResult:
|
|
|
282
282
|
sprint_result = validate_sprint(data)
|
|
283
283
|
result.merge(sprint_result)
|
|
284
284
|
|
|
285
|
-
# Validate epics
|
|
285
|
+
# Validate epics (skip if sharded — epics are string refs to shard files)
|
|
286
286
|
if "epics" in data:
|
|
287
287
|
all_story_ids: set[str] = set()
|
|
288
288
|
for idx, epic in enumerate(data["epics"]):
|
|
289
|
+
# Sharded format: epics are string refs, not dicts
|
|
290
|
+
if isinstance(epic, str):
|
|
291
|
+
continue
|
|
289
292
|
epic_result = validate_epic(epic, all_story_ids, idx)
|
|
290
293
|
result.merge(epic_result)
|
|
291
294
|
|
|
@@ -349,6 +352,9 @@ def validate_future(data: dict[str, Any]) -> ValidationResult:
|
|
|
349
352
|
return result
|
|
350
353
|
|
|
351
354
|
for i, initiative in enumerate(initiatives):
|
|
355
|
+
# Sharded format: initiatives are string slugs, not dicts
|
|
356
|
+
if isinstance(initiative, str):
|
|
357
|
+
continue
|
|
352
358
|
if not isinstance(initiative, dict):
|
|
353
359
|
result.add_error(f"Initiative must be a mapping", f"future.initiatives[{i}]")
|
|
354
360
|
continue
|
|
@@ -503,6 +509,10 @@ def validate_sprint_file(file_path: Path) -> ValidationResult:
|
|
|
503
509
|
)
|
|
504
510
|
return result
|
|
505
511
|
|
|
512
|
+
# Merge sharded epic files if present
|
|
513
|
+
from pennyfarthing_scripts.sprint.loader import _merge_epic_shards
|
|
514
|
+
data = _merge_epic_shards(data, file_path.parent)
|
|
515
|
+
|
|
506
516
|
# Validate loaded data
|
|
507
517
|
return validate_full_sprint(data)
|
|
508
518
|
|
|
@@ -35,6 +35,20 @@ def check_story(story_id: str) -> dict[str, Any]:
|
|
|
35
35
|
status = story.get("status", "backlog")
|
|
36
36
|
assigned = story.get("assigned_to")
|
|
37
37
|
|
|
38
|
+
# Check if assigned to someone else
|
|
39
|
+
if assigned:
|
|
40
|
+
from pennyfarthing_scripts.jira.client import get_current_user_email
|
|
41
|
+
|
|
42
|
+
current_user = get_current_user_email()
|
|
43
|
+
if assigned != current_user:
|
|
44
|
+
return {
|
|
45
|
+
"available": False,
|
|
46
|
+
"type": "story",
|
|
47
|
+
"story": story,
|
|
48
|
+
"reason": f"Assigned to {assigned}",
|
|
49
|
+
"assigned_to": assigned,
|
|
50
|
+
}
|
|
51
|
+
|
|
38
52
|
# Check if already in progress
|
|
39
53
|
if status == "in_progress":
|
|
40
54
|
return {
|
|
@@ -54,6 +68,16 @@ def check_story(story_id: str) -> dict[str, Any]:
|
|
|
54
68
|
"reason": "Already completed",
|
|
55
69
|
}
|
|
56
70
|
|
|
71
|
+
# Check if canceled
|
|
72
|
+
if status == "canceled":
|
|
73
|
+
return {
|
|
74
|
+
"available": False,
|
|
75
|
+
"type": "story",
|
|
76
|
+
"story": story,
|
|
77
|
+
"reason": "Story is canceled",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Available statuses: backlog, ready, planning
|
|
57
81
|
return {
|
|
58
82
|
"available": True,
|
|
59
83
|
"type": "story",
|
|
@@ -67,10 +91,23 @@ def check_story(story_id: str) -> dict[str, Any]:
|
|
|
67
91
|
def get_next_story() -> dict[str, Any]:
|
|
68
92
|
"""Get the highest priority available story.
|
|
69
93
|
|
|
94
|
+
Considers stories with backlog, ready, or planning status.
|
|
95
|
+
Excludes stories assigned to other users.
|
|
96
|
+
|
|
70
97
|
Returns:
|
|
71
98
|
Dict with next story details or error
|
|
72
99
|
"""
|
|
73
|
-
|
|
100
|
+
from pennyfarthing_scripts.jira.client import get_current_user_email
|
|
101
|
+
from pennyfarthing_scripts.sprint.loader import get_all_stories
|
|
102
|
+
|
|
103
|
+
current_user = get_current_user_email()
|
|
104
|
+
all_stories = get_all_stories()
|
|
105
|
+
available_statuses = {"backlog", "ready", "planning"}
|
|
106
|
+
backlog = [
|
|
107
|
+
s for s in all_stories
|
|
108
|
+
if s.get("status") in available_statuses
|
|
109
|
+
and (not s.get("assigned_to") or s.get("assigned_to") == current_user)
|
|
110
|
+
]
|
|
74
111
|
|
|
75
112
|
if not backlog:
|
|
76
113
|
return {
|
|
@@ -78,11 +115,14 @@ def get_next_story() -> dict[str, Any]:
|
|
|
78
115
|
"error": "No stories in backlog",
|
|
79
116
|
}
|
|
80
117
|
|
|
81
|
-
# Sort by priority (P0 > P1 > P2 > P3)
|
|
118
|
+
# Sort by priority (P0 > P1 > P2 > P3), preferring own assignments
|
|
82
119
|
priority_order = {"P0": 0, "P1": 1, "P2": 2, "P3": 3}
|
|
83
120
|
sorted_stories = sorted(
|
|
84
121
|
backlog,
|
|
85
|
-
key=lambda s:
|
|
122
|
+
key=lambda s: (
|
|
123
|
+
0 if s.get("assigned_to") == current_user else 1,
|
|
124
|
+
priority_order.get(s.get("priority", "P2"), 2),
|
|
125
|
+
),
|
|
86
126
|
)
|
|
87
127
|
|
|
88
128
|
next_story = sorted_stories[0]
|
|
@@ -4,13 +4,14 @@ Deterministic YAML I/O for sprint data.
|
|
|
4
4
|
Story: MSSCI-14254 - Core yaml_io module with deterministic serialization
|
|
5
5
|
|
|
6
6
|
Provides:
|
|
7
|
-
- read_sprint(path) -> CommentedMap (preserves ordering/comments)
|
|
8
|
-
- write_sprint(path, data) -> atomic write
|
|
7
|
+
- read_sprint(path) -> CommentedMap (preserves ordering/comments, merges shards)
|
|
8
|
+
- write_sprint(path, data) -> atomic write (shard-aware)
|
|
9
9
|
- canonical_dump(data) -> deterministic YAML string
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import io
|
|
13
13
|
import os
|
|
14
|
+
import re
|
|
14
15
|
from collections.abc import Mapping
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
from typing import Any
|
|
@@ -19,6 +20,8 @@ from ruamel.yaml import YAML
|
|
|
19
20
|
from ruamel.yaml.comments import CommentedMap, CommentedSeq
|
|
20
21
|
from ruamel.yaml.scalarstring import LiteralScalarString
|
|
21
22
|
|
|
23
|
+
JIRA_PATTERN = re.compile(r"^MSSCI-\d{5}$")
|
|
24
|
+
|
|
22
25
|
|
|
23
26
|
# Canonical key ordering derived from sprint-template.yaml
|
|
24
27
|
SPRINT_KEY_ORDER: list[str] = [
|
|
@@ -52,11 +55,11 @@ def _make_yaml() -> YAML:
|
|
|
52
55
|
return yml
|
|
53
56
|
|
|
54
57
|
|
|
55
|
-
def
|
|
56
|
-
"""Read
|
|
58
|
+
def _read_yaml_file(path: Path) -> CommentedMap:
|
|
59
|
+
"""Read a single YAML file preserving ordering and comments.
|
|
57
60
|
|
|
58
61
|
Args:
|
|
59
|
-
path: Path to
|
|
62
|
+
path: Path to YAML file
|
|
60
63
|
|
|
61
64
|
Returns:
|
|
62
65
|
CommentedMap with preserved ordering and comments
|
|
@@ -81,6 +84,44 @@ def read_sprint(path: Path) -> CommentedMap:
|
|
|
81
84
|
return data
|
|
82
85
|
|
|
83
86
|
|
|
87
|
+
def read_sprint(path: Path) -> CommentedMap:
|
|
88
|
+
"""Read sprint YAML file, merging sharded epic files.
|
|
89
|
+
|
|
90
|
+
When the epics list contains string references (sharded format),
|
|
91
|
+
loads each epic-{ref}.yaml shard file and replaces the strings
|
|
92
|
+
with full epic CommentedMaps.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
path: Path to sprint YAML index file
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
CommentedMap with full epic data merged in
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
FileNotFoundError: If path doesn't exist
|
|
102
|
+
ValueError: If YAML is malformed or empty
|
|
103
|
+
"""
|
|
104
|
+
data = _read_yaml_file(path)
|
|
105
|
+
|
|
106
|
+
epics = data.get("epics", [])
|
|
107
|
+
if not epics or not isinstance(epics[0], str):
|
|
108
|
+
return data
|
|
109
|
+
|
|
110
|
+
sprint_dir = path.parent
|
|
111
|
+
merged_epics = CommentedSeq()
|
|
112
|
+
for ref in epics:
|
|
113
|
+
if isinstance(ref, str):
|
|
114
|
+
shard_file = sprint_dir / f"epic-{ref}.yaml"
|
|
115
|
+
if shard_file.exists():
|
|
116
|
+
epic_data = _read_yaml_file(shard_file)
|
|
117
|
+
merged_epics.append(epic_data)
|
|
118
|
+
else:
|
|
119
|
+
merged_epics.append(ref)
|
|
120
|
+
|
|
121
|
+
data["epics"] = merged_epics
|
|
122
|
+
return data
|
|
123
|
+
|
|
124
|
+
|
|
84
125
|
def _sort_mapping(data: CommentedMap, key_order: list[str]) -> CommentedMap:
|
|
85
126
|
"""Reorder keys in a CommentedMap according to key_order.
|
|
86
127
|
|
|
@@ -228,18 +269,25 @@ def canonical_dump(data: Any) -> str:
|
|
|
228
269
|
return result
|
|
229
270
|
|
|
230
271
|
|
|
231
|
-
def
|
|
232
|
-
"""
|
|
272
|
+
def _get_epic_ref(epic: Mapping) -> str:
|
|
273
|
+
"""Get the canonical reference ID for an epic shard file.
|
|
233
274
|
|
|
234
|
-
|
|
275
|
+
Mirrors the logic in migrate-to-shards.py: prefer Jira key, fall back to ID.
|
|
276
|
+
"""
|
|
277
|
+
jira = epic.get("jira")
|
|
278
|
+
epic_id = str(epic.get("id", ""))
|
|
235
279
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
280
|
+
if jira and JIRA_PATTERN.match(str(jira)):
|
|
281
|
+
return str(jira)
|
|
282
|
+
if JIRA_PATTERN.match(epic_id):
|
|
283
|
+
return epic_id
|
|
284
|
+
return epic_id
|
|
239
285
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
286
|
+
|
|
287
|
+
def _write_yaml_file(path: Path, data: Any) -> None:
|
|
288
|
+
"""Write data to a single YAML file atomically.
|
|
289
|
+
|
|
290
|
+
Uses temp file + os.replace() for atomic writes on POSIX.
|
|
243
291
|
"""
|
|
244
292
|
if not isinstance(data, Mapping):
|
|
245
293
|
raise TypeError(f"Expected mapping type, got {type(data).__name__}")
|
|
@@ -252,7 +300,68 @@ def write_sprint(path: Path, data: Any) -> None:
|
|
|
252
300
|
f.write(output)
|
|
253
301
|
os.replace(tmp_path, path)
|
|
254
302
|
except Exception:
|
|
255
|
-
# Clean up temp file if it exists
|
|
256
303
|
if tmp_path.exists():
|
|
257
304
|
tmp_path.unlink()
|
|
258
305
|
raise
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _is_sharded_on_disk(path: Path) -> bool:
|
|
309
|
+
"""Check if the on-disk index file uses sharded epic references."""
|
|
310
|
+
if not path.exists():
|
|
311
|
+
return False
|
|
312
|
+
yml = _make_yaml()
|
|
313
|
+
try:
|
|
314
|
+
with open(path) as f:
|
|
315
|
+
on_disk = yml.load(f)
|
|
316
|
+
except Exception:
|
|
317
|
+
return False
|
|
318
|
+
if on_disk is None or not isinstance(on_disk, Mapping):
|
|
319
|
+
return False
|
|
320
|
+
epics = on_disk.get("epics", [])
|
|
321
|
+
return bool(epics) and isinstance(epics[0], str)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def write_sprint(path: Path, data: Any) -> None:
|
|
325
|
+
"""Write sprint data to YAML file(s) atomically.
|
|
326
|
+
|
|
327
|
+
If the on-disk index uses sharded format (epics as string refs),
|
|
328
|
+
writes each epic to its shard file and the index with string refs.
|
|
329
|
+
Otherwise writes the full data to a single file.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
path: Destination path (the index file)
|
|
333
|
+
data: Sprint data (CommentedMap or dict)
|
|
334
|
+
|
|
335
|
+
Raises:
|
|
336
|
+
TypeError: If data is not a valid mapping type
|
|
337
|
+
OSError: If write fails
|
|
338
|
+
"""
|
|
339
|
+
if not isinstance(data, Mapping):
|
|
340
|
+
raise TypeError(f"Expected mapping type, got {type(data).__name__}")
|
|
341
|
+
|
|
342
|
+
if not _is_sharded_on_disk(path):
|
|
343
|
+
_write_yaml_file(path, data)
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
# Sharded write: each epic goes to its own file
|
|
347
|
+
sprint_dir = path.parent
|
|
348
|
+
epic_refs = CommentedSeq()
|
|
349
|
+
|
|
350
|
+
for epic in data.get("epics", []):
|
|
351
|
+
if isinstance(epic, Mapping):
|
|
352
|
+
ref = _get_epic_ref(epic)
|
|
353
|
+
shard_file = sprint_dir / f"epic-{ref}.yaml"
|
|
354
|
+
_write_yaml_file(shard_file, epic)
|
|
355
|
+
epic_refs.append(ref)
|
|
356
|
+
else:
|
|
357
|
+
epic_refs.append(epic)
|
|
358
|
+
|
|
359
|
+
# Write index with string refs instead of full epic dicts
|
|
360
|
+
index = CommentedMap()
|
|
361
|
+
for key in data:
|
|
362
|
+
if key == "epics":
|
|
363
|
+
index["epics"] = epic_refs
|
|
364
|
+
else:
|
|
365
|
+
index[key] = data[key]
|
|
366
|
+
|
|
367
|
+
_write_yaml_file(path, index)
|
|
Binary file
|
|
Binary file
|
package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc
CHANGED
|
Binary file
|
package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc
CHANGED
|
Binary file
|
|
Binary file
|
package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc
CHANGED
|
Binary file
|