@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,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for health score analysis results.
|
|
3
|
+
|
|
4
|
+
Follows ADR-0008 result pattern — structured dataclasses with success/error fields.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Default weights for each dimension (must sum to 1.0)
|
|
13
|
+
DEFAULT_WEIGHTS: dict[str, float] = {
|
|
14
|
+
"churn": 0.15,
|
|
15
|
+
"todo_density": 0.15,
|
|
16
|
+
"complexity": 0.15,
|
|
17
|
+
"test_gaps": 0.15,
|
|
18
|
+
"dead_code": 0.10,
|
|
19
|
+
"deprecation_debt": 0.10,
|
|
20
|
+
"dependency_freshness": 0.10,
|
|
21
|
+
"agent_context_efficiency": 0.10,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class DimensionScore:
|
|
27
|
+
"""Score for a single health dimension."""
|
|
28
|
+
|
|
29
|
+
name: str
|
|
30
|
+
score: float | None = None # 0-100, None if unavailable
|
|
31
|
+
weight: float = 0.0
|
|
32
|
+
error: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class HealthscoreResult:
|
|
37
|
+
"""Composite health score following ADR-0008 pattern."""
|
|
38
|
+
|
|
39
|
+
success: bool
|
|
40
|
+
composite_score: float = 0.0 # 0-100 weighted average
|
|
41
|
+
target_path: str = ""
|
|
42
|
+
dimensions: list[DimensionScore] = field(default_factory=list)
|
|
43
|
+
cached: bool = False
|
|
44
|
+
error: str | None = None
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -39,6 +39,26 @@ DEFAULT_EXCLUDES = [
|
|
|
39
39
|
"*.map",
|
|
40
40
|
"package-lock.json",
|
|
41
41
|
"pnpm-lock.yaml",
|
|
42
|
+
# Dotfiles
|
|
43
|
+
".*",
|
|
44
|
+
# Images
|
|
45
|
+
"*.png",
|
|
46
|
+
"*.jpg",
|
|
47
|
+
"*.jpeg",
|
|
48
|
+
"*.gif",
|
|
49
|
+
"*.svg",
|
|
50
|
+
"*.ico",
|
|
51
|
+
# Fonts
|
|
52
|
+
"*.woff",
|
|
53
|
+
"*.woff2",
|
|
54
|
+
"*.ttf",
|
|
55
|
+
"*.eot",
|
|
56
|
+
# Generated files
|
|
57
|
+
"*.d.ts",
|
|
58
|
+
"*.snap",
|
|
59
|
+
"*.d.ts.map",
|
|
60
|
+
# CI config
|
|
61
|
+
".github/*",
|
|
42
62
|
]
|
|
43
63
|
|
|
44
64
|
# Regex for identifying bug-fix commits
|
|
@@ -422,6 +442,7 @@ async def analyze_all_repos(
|
|
|
422
442
|
days: int = 90,
|
|
423
443
|
excludes: list[str] | None = None,
|
|
424
444
|
branch: str = "--all",
|
|
445
|
+
skip_types: list[str] | None = None,
|
|
425
446
|
) -> MultiRepoHotspotResult:
|
|
426
447
|
"""Analyze all repos found under project root in parallel.
|
|
427
448
|
|
|
@@ -432,13 +453,14 @@ async def analyze_all_repos(
|
|
|
432
453
|
days: Time window in days
|
|
433
454
|
excludes: Additional file patterns to exclude
|
|
434
455
|
branch: Branch spec
|
|
456
|
+
skip_types: Repo types to exclude (e.g. ["orchestrator"])
|
|
435
457
|
|
|
436
458
|
Returns:
|
|
437
459
|
MultiRepoHotspotResult with per-repo results
|
|
438
460
|
"""
|
|
439
461
|
from pennyfarthing_scripts.common.config import load_yaml_config
|
|
440
462
|
|
|
441
|
-
repos_yaml = load_yaml_config(project_root / "repos.yaml")
|
|
463
|
+
repos_yaml = load_yaml_config(project_root / ".pennyfarthing" / "repos.yaml")
|
|
442
464
|
|
|
443
465
|
repos: list[tuple[str, Path]] = []
|
|
444
466
|
|
|
@@ -446,6 +468,11 @@ async def analyze_all_repos(
|
|
|
446
468
|
# Extract repos from repos.yaml
|
|
447
469
|
for repo_name, repo_config in repos_yaml.items():
|
|
448
470
|
if isinstance(repo_config, dict):
|
|
471
|
+
# Filter by type if skip_types is provided
|
|
472
|
+
if skip_types:
|
|
473
|
+
repo_type = repo_config.get("type")
|
|
474
|
+
if repo_type and repo_type in skip_types:
|
|
475
|
+
continue
|
|
449
476
|
repo_path = repo_config.get("path", repo_name)
|
|
450
477
|
else:
|
|
451
478
|
repo_path = str(repo_config)
|
|
@@ -38,15 +38,17 @@ def _common_options(fn):
|
|
|
38
38
|
fn = click.option("--output", "output_file", type=click.Path(), help="Write output to file")(fn)
|
|
39
39
|
fn = click.option("--exclude", multiple=True, help="Additional exclude patterns (repeatable)")(fn)
|
|
40
40
|
fn = click.option("--branch", default="--all", show_default=True, help="Branch spec for git log")(fn)
|
|
41
|
+
fn = click.option("--skip-type", "skip_type", multiple=True, help="Skip repos by type (repeatable, e.g. --skip-type orchestrator)")(fn)
|
|
41
42
|
return fn
|
|
42
43
|
|
|
43
44
|
|
|
44
|
-
def _run_analysis(repo: str | None, repo_path: str | None, days: int, exclude: tuple, branch: str):
|
|
45
|
+
def _run_analysis(repo: str | None, repo_path: str | None, days: int, exclude: tuple, branch: str, skip_type: tuple = ()):
|
|
45
46
|
"""Run analysis and return result."""
|
|
46
47
|
from pennyfarthing_scripts.hotspots.analyze import analyze_all_repos, analyze_repo
|
|
47
48
|
from pennyfarthing_scripts.common.config import get_project_root
|
|
48
49
|
|
|
49
50
|
excludes = list(exclude) if exclude else None
|
|
51
|
+
skip_types = list(skip_type) if skip_type else None
|
|
50
52
|
|
|
51
53
|
if repo_path:
|
|
52
54
|
# Standalone analysis of a specific path
|
|
@@ -56,7 +58,7 @@ def _run_analysis(repo: str | None, repo_path: str | None, days: int, exclude: t
|
|
|
56
58
|
# Single named repo from project
|
|
57
59
|
project_root = get_project_root()
|
|
58
60
|
from pennyfarthing_scripts.common.config import load_yaml_config
|
|
59
|
-
repos_yaml = load_yaml_config(project_root / "repos.yaml")
|
|
61
|
+
repos_yaml = load_yaml_config(project_root / ".pennyfarthing" / "repos.yaml")
|
|
60
62
|
if repos_yaml and repo in repos_yaml:
|
|
61
63
|
cfg = repos_yaml[repo]
|
|
62
64
|
rpath = cfg.get("path", repo) if isinstance(cfg, dict) else str(cfg)
|
|
@@ -75,7 +77,7 @@ def _run_analysis(repo: str | None, repo_path: str | None, days: int, exclude: t
|
|
|
75
77
|
# All repos
|
|
76
78
|
project_root = get_project_root()
|
|
77
79
|
return asyncio.run(
|
|
78
|
-
analyze_all_repos(project_root, days, excludes, branch)
|
|
80
|
+
analyze_all_repos(project_root, days, excludes, branch, skip_types)
|
|
79
81
|
)
|
|
80
82
|
|
|
81
83
|
|
|
@@ -130,23 +132,23 @@ def _output_result(result, fmt: str, output_file: str | None, top: int, mode: st
|
|
|
130
132
|
|
|
131
133
|
@hotspots.command()
|
|
132
134
|
@_common_options
|
|
133
|
-
def analyze(repo, repo_path, days, top, fmt, output_file, exclude, branch):
|
|
135
|
+
def analyze(repo, repo_path, days, top, fmt, output_file, exclude, branch, skip_type):
|
|
134
136
|
"""Full hotspot analysis — files and directories."""
|
|
135
|
-
result = _run_analysis(repo, repo_path, days, exclude, branch)
|
|
137
|
+
result = _run_analysis(repo, repo_path, days, exclude, branch, skip_type)
|
|
136
138
|
_output_result(result, fmt, output_file, top, "analyze")
|
|
137
139
|
|
|
138
140
|
|
|
139
141
|
@hotspots.command()
|
|
140
142
|
@_common_options
|
|
141
|
-
def files(repo, repo_path, days, top, fmt, output_file, exclude, branch):
|
|
143
|
+
def files(repo, repo_path, days, top, fmt, output_file, exclude, branch, skip_type):
|
|
142
144
|
"""File-level hotspot report."""
|
|
143
|
-
result = _run_analysis(repo, repo_path, days, exclude, branch)
|
|
145
|
+
result = _run_analysis(repo, repo_path, days, exclude, branch, skip_type)
|
|
144
146
|
_output_result(result, fmt, output_file, top, "files")
|
|
145
147
|
|
|
146
148
|
|
|
147
149
|
@hotspots.command()
|
|
148
150
|
@_common_options
|
|
149
|
-
def dirs(repo, repo_path, days, top, fmt, output_file, exclude, branch):
|
|
151
|
+
def dirs(repo, repo_path, days, top, fmt, output_file, exclude, branch, skip_type):
|
|
150
152
|
"""Directory-level hotspot report."""
|
|
151
|
-
result = _run_analysis(repo, repo_path, days, exclude, branch)
|
|
153
|
+
result = _run_analysis(repo, repo_path, days, exclude, branch, skip_type)
|
|
152
154
|
_output_result(result, fmt, output_file, top, "dirs")
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -43,7 +43,7 @@ class SyncChange:
|
|
|
43
43
|
"""Represents a single sync change to apply."""
|
|
44
44
|
|
|
45
45
|
key: str
|
|
46
|
-
field: Literal["status", "points"]
|
|
46
|
+
field: Literal["status", "points", "assigned_to"]
|
|
47
47
|
action: Literal["update-yaml", "update-jira"]
|
|
48
48
|
yaml_value: Any
|
|
49
49
|
jira_value: Any
|
|
@@ -123,10 +123,15 @@ Examples:
|
|
|
123
123
|
action="store_true",
|
|
124
124
|
help="Sync story points",
|
|
125
125
|
)
|
|
126
|
+
parser.add_argument(
|
|
127
|
+
"--assignee",
|
|
128
|
+
action="store_true",
|
|
129
|
+
help="Sync assignee field (Jira -> YAML only)",
|
|
130
|
+
)
|
|
126
131
|
parser.add_argument(
|
|
127
132
|
"--all",
|
|
128
133
|
action="store_true",
|
|
129
|
-
help="Sync all fields (status + points)",
|
|
134
|
+
help="Sync all fields (status + points + assignee)",
|
|
130
135
|
)
|
|
131
136
|
parser.add_argument(
|
|
132
137
|
"--sprint",
|
|
@@ -137,10 +142,11 @@ Examples:
|
|
|
137
142
|
|
|
138
143
|
args = parser.parse_args(argv)
|
|
139
144
|
|
|
140
|
-
# --all implies
|
|
145
|
+
# --all implies --status, --points, and --assignee
|
|
141
146
|
if args.all:
|
|
142
147
|
args.status = True
|
|
143
148
|
args.points = True
|
|
149
|
+
args.assignee = True
|
|
144
150
|
|
|
145
151
|
return args
|
|
146
152
|
|
|
@@ -156,6 +162,7 @@ def generate_sync_plan(
|
|
|
156
162
|
*,
|
|
157
163
|
sync_status: bool = False,
|
|
158
164
|
sync_points: bool = False,
|
|
165
|
+
sync_assignee: bool = False,
|
|
159
166
|
yaml_wins: bool = False,
|
|
160
167
|
) -> SyncPlan:
|
|
161
168
|
"""Generate a sync plan comparing YAML and Jira stories.
|
|
@@ -165,6 +172,7 @@ def generate_sync_plan(
|
|
|
165
172
|
jira_stories: Stories from Jira [{key, fields: {status: {name}, customfield_10031, ...}}]
|
|
166
173
|
sync_status: Whether to sync status field
|
|
167
174
|
sync_points: Whether to sync points field
|
|
175
|
+
sync_assignee: Whether to sync assignee field (always Jira -> YAML)
|
|
168
176
|
yaml_wins: If True, YAML wins conflicts (default: Jira wins)
|
|
169
177
|
|
|
170
178
|
Returns:
|
|
@@ -248,6 +256,26 @@ def generate_sync_plan(
|
|
|
248
256
|
)
|
|
249
257
|
)
|
|
250
258
|
|
|
259
|
+
# Check assignee differences (always Jira -> YAML, ignores yaml_wins)
|
|
260
|
+
if sync_assignee:
|
|
261
|
+
yaml_assignee = yaml_story.get("assigned_to")
|
|
262
|
+
jira_assignee_obj = jira_story.get("fields", {}).get("assignee")
|
|
263
|
+
jira_assignee_email = (
|
|
264
|
+
jira_assignee_obj.get("emailAddress") if jira_assignee_obj else None
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if yaml_assignee != jira_assignee_email:
|
|
268
|
+
plan.changes.append(
|
|
269
|
+
SyncChange(
|
|
270
|
+
key=key,
|
|
271
|
+
field="assigned_to",
|
|
272
|
+
action="update-yaml",
|
|
273
|
+
yaml_value=yaml_assignee,
|
|
274
|
+
jira_value=jira_assignee_email,
|
|
275
|
+
target_value=jira_assignee_email,
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
|
|
251
279
|
return plan
|
|
252
280
|
|
|
253
281
|
|
|
@@ -326,21 +354,17 @@ async def execute_sync_plan(
|
|
|
326
354
|
|
|
327
355
|
# Execute YAML updates (sequential, file-based)
|
|
328
356
|
if yaml_updates:
|
|
329
|
-
# Load current sprint data
|
|
330
357
|
from pennyfarthing_scripts.common.config import get_project_root
|
|
358
|
+
from pennyfarthing_scripts.sprint.yaml_io import read_sprint, write_sprint
|
|
331
359
|
|
|
332
360
|
root = get_project_root()
|
|
333
361
|
sprint_file = sprint_path or (root / "sprint" / "current-sprint.yaml")
|
|
334
362
|
|
|
335
363
|
if sprint_file.exists():
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
with open(sprint_file) as f:
|
|
339
|
-
sprint_data = yaml.safe_load(f)
|
|
364
|
+
sprint_data = read_sprint(sprint_file)
|
|
340
365
|
|
|
341
366
|
# Apply YAML updates
|
|
342
367
|
for change in yaml_updates:
|
|
343
|
-
# Find and update the story in sprint data
|
|
344
368
|
updated = _update_story_in_sprint(
|
|
345
369
|
sprint_data, change.key, change.field, change.target_value
|
|
346
370
|
)
|
|
@@ -348,10 +372,9 @@ async def execute_sync_plan(
|
|
|
348
372
|
result.changes_applied += 1
|
|
349
373
|
result.yaml_modified = True
|
|
350
374
|
|
|
351
|
-
# Write back if modified
|
|
375
|
+
# Write back if modified (handles shards automatically)
|
|
352
376
|
if result.yaml_modified:
|
|
353
|
-
|
|
354
|
-
yaml.dump(sprint_data, f, default_flow_style=False, sort_keys=False)
|
|
377
|
+
write_sprint(sprint_file, sprint_data)
|
|
355
378
|
|
|
356
379
|
return result
|
|
357
380
|
|
|
@@ -379,7 +402,10 @@ def _update_story_in_sprint(
|
|
|
379
402
|
for epic in sprint_data.get("epics", []):
|
|
380
403
|
for story in epic.get("stories", []):
|
|
381
404
|
if story.get("jira") == jira_key:
|
|
382
|
-
|
|
405
|
+
if value is None and field in story:
|
|
406
|
+
del story[field]
|
|
407
|
+
elif value is not None:
|
|
408
|
+
story[field] = value
|
|
383
409
|
return True
|
|
384
410
|
|
|
385
411
|
return False
|
|
@@ -465,8 +491,8 @@ async def async_main(args: argparse.Namespace) -> int:
|
|
|
465
491
|
Exit code (0 for success, 1 for error)
|
|
466
492
|
"""
|
|
467
493
|
# Validate at least one field is selected
|
|
468
|
-
if not args.status and not args.points:
|
|
469
|
-
error("Must specify at least one field to sync: --status, --points, or --all")
|
|
494
|
+
if not args.status and not args.points and not args.assignee:
|
|
495
|
+
error("Must specify at least one field to sync: --status, --points, --assignee, or --all")
|
|
470
496
|
return 1
|
|
471
497
|
|
|
472
498
|
# Load sprint data
|
|
@@ -510,6 +536,7 @@ async def async_main(args: argparse.Namespace) -> int:
|
|
|
510
536
|
jira_stories,
|
|
511
537
|
sync_status=args.status,
|
|
512
538
|
sync_points=args.points,
|
|
539
|
+
sync_assignee=args.assignee,
|
|
513
540
|
yaml_wins=args.yaml_wins,
|
|
514
541
|
)
|
|
515
542
|
|
|
@@ -175,6 +175,80 @@ def create_story(epic_jira_key, story_id, dry_run):
|
|
|
175
175
|
raise SystemExit(1)
|
|
176
176
|
|
|
177
177
|
|
|
178
|
+
@create.command("standalone")
|
|
179
|
+
@click.argument("title")
|
|
180
|
+
@click.option("--points", default=2, type=int, help="Story points (default: 2)")
|
|
181
|
+
@click.option("--description", "-d", default="", help="Story description")
|
|
182
|
+
@click.option("--dry-run", is_flag=True, help="Preview without creating")
|
|
183
|
+
def create_standalone(title, points, description, dry_run):
|
|
184
|
+
"""Create a standalone Jira story, add to sprint, mark Done.
|
|
185
|
+
|
|
186
|
+
Uses REST API directly — no interactive prompts, no stdin issues.
|
|
187
|
+
|
|
188
|
+
\b
|
|
189
|
+
Arguments:
|
|
190
|
+
TITLE - Story summary
|
|
191
|
+
|
|
192
|
+
\b
|
|
193
|
+
Examples:
|
|
194
|
+
pf jira create standalone "Fix sprint script shard support" --points 3
|
|
195
|
+
pf jira create standalone "Add drift detection" -d "Detects YAML drift"
|
|
196
|
+
pf jira create standalone "Quick fix" --dry-run
|
|
197
|
+
"""
|
|
198
|
+
from pennyfarthing_scripts.jira.client import JIRA_PROJECT, get_client
|
|
199
|
+
from pennyfarthing_scripts.jira.create import _build_adf_description
|
|
200
|
+
from pennyfarthing_scripts.sprint.loader import get_sprint_info
|
|
201
|
+
|
|
202
|
+
sprint_info = get_sprint_info()
|
|
203
|
+
sprint_id = sprint_info.get("jira_sprint_id")
|
|
204
|
+
|
|
205
|
+
if dry_run:
|
|
206
|
+
click.echo(f"[DRY-RUN] Would create: {title}")
|
|
207
|
+
click.echo(f" Points: {points}")
|
|
208
|
+
click.echo(f" Sprint: {sprint_id}")
|
|
209
|
+
click.echo(f" Actions: create -> add to sprint -> transition to Done")
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
client = get_client()
|
|
213
|
+
|
|
214
|
+
# 1. Create the story
|
|
215
|
+
payload = {
|
|
216
|
+
"fields": {
|
|
217
|
+
"project": {"key": JIRA_PROJECT},
|
|
218
|
+
"summary": title,
|
|
219
|
+
"description": _build_adf_description(description),
|
|
220
|
+
"issuetype": {"name": "Story"},
|
|
221
|
+
"labels": ["pennyfarthing"],
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
response = client.create_issue_sync(payload)
|
|
226
|
+
if not response or "key" not in response:
|
|
227
|
+
raise click.ClickException(f"Failed to create story: {response}")
|
|
228
|
+
|
|
229
|
+
jira_key = response["key"]
|
|
230
|
+
click.echo(f"Created: {jira_key}")
|
|
231
|
+
|
|
232
|
+
# 2. Set story points
|
|
233
|
+
if points > 0:
|
|
234
|
+
client.update_issue_sync(jira_key, {"customfield_10031": points})
|
|
235
|
+
|
|
236
|
+
# 3. Add to sprint
|
|
237
|
+
if sprint_id:
|
|
238
|
+
client.add_to_sprint_sync(sprint_id, jira_key)
|
|
239
|
+
click.echo(f"Added to sprint {sprint_id}")
|
|
240
|
+
|
|
241
|
+
# 4. Transition to Done
|
|
242
|
+
result = client.transition_sync(jira_key, "Done")
|
|
243
|
+
if result.get("success"):
|
|
244
|
+
click.echo(f"Transitioned to Done")
|
|
245
|
+
else:
|
|
246
|
+
click.echo(f"Warning: could not transition to Done: {result.get('reason')}")
|
|
247
|
+
|
|
248
|
+
click.echo(f"\n{jira_key}: {title}")
|
|
249
|
+
click.echo(f"https://1898andco.atlassian.net/browse/{jira_key}")
|
|
250
|
+
|
|
251
|
+
|
|
178
252
|
@jira.command()
|
|
179
253
|
@click.argument("epic")
|
|
180
254
|
@click.option("--dry-run", is_flag=True, help="Preview without applying")
|
|
@@ -200,9 +274,10 @@ def sync(epic, dry_run, transition, points, sync_all):
|
|
|
200
274
|
@click.option("--yaml-wins", is_flag=True, help="Prefer YAML values on conflict")
|
|
201
275
|
@click.option("--status", is_flag=True, help="Sync status field")
|
|
202
276
|
@click.option("--points", is_flag=True, help="Sync story points")
|
|
277
|
+
@click.option("--assignee", is_flag=True, help="Sync assignee field (Jira -> YAML only)")
|
|
203
278
|
@click.option("--all", "sync_all", is_flag=True, help="Sync all fields")
|
|
204
279
|
@click.option("--sprint", "sprint_id", help="Target specific sprint")
|
|
205
|
-
def bidirectional(dry_run, yaml_wins, status, points, sync_all, sprint_id):
|
|
280
|
+
def bidirectional(dry_run, yaml_wins, status, points, assignee, sync_all, sprint_id):
|
|
206
281
|
"""Bidirectional sync between YAML and Jira."""
|
|
207
282
|
from pennyfarthing_scripts.jira.bidirectional import main as bidirectional_main
|
|
208
283
|
|
|
@@ -215,6 +290,8 @@ def bidirectional(dry_run, yaml_wins, status, points, sync_all, sprint_id):
|
|
|
215
290
|
args.append("--status")
|
|
216
291
|
if points or sync_all:
|
|
217
292
|
args.append("--points")
|
|
293
|
+
if assignee or sync_all:
|
|
294
|
+
args.append("--assignee")
|
|
218
295
|
if sync_all:
|
|
219
296
|
args.append("--all")
|
|
220
297
|
if sprint_id:
|
|
@@ -301,6 +301,34 @@ def map_github_to_jira(github_user: str | None) -> str | None:
|
|
|
301
301
|
return GITHUB_TO_JIRA_MAP.get(github_user, f"{github_user}@1898andco.io")
|
|
302
302
|
|
|
303
303
|
|
|
304
|
+
def get_current_user_email() -> str:
|
|
305
|
+
"""Get the current user's Jira email address.
|
|
306
|
+
|
|
307
|
+
Resolution order:
|
|
308
|
+
1. JIRA_USER environment variable
|
|
309
|
+
2. git config user.email
|
|
310
|
+
3. Default fallback
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Email address string
|
|
314
|
+
"""
|
|
315
|
+
jira_user = os.environ.get("JIRA_USER")
|
|
316
|
+
if jira_user:
|
|
317
|
+
return jira_user
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
result = subprocess.run(
|
|
321
|
+
["git", "config", "user.email"],
|
|
322
|
+
capture_output=True, text=True, timeout=5,
|
|
323
|
+
)
|
|
324
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
325
|
+
return result.stdout.strip()
|
|
326
|
+
except Exception:
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
return "keith.avery@1898andco.io"
|
|
330
|
+
|
|
331
|
+
|
|
304
332
|
# =============================================================================
|
|
305
333
|
# JiraClient - Unified REST API client
|
|
306
334
|
# =============================================================================
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -154,13 +154,13 @@ def get_phase_owner(workflow: str, phase: str, project_root: Path) -> str | None
|
|
|
154
154
|
|
|
155
155
|
|
|
156
156
|
def get_backlog_count(project_root: Path) -> int:
|
|
157
|
-
"""Count stories in backlog or
|
|
157
|
+
"""Count stories in backlog, ready, or planning status.
|
|
158
158
|
|
|
159
159
|
Args:
|
|
160
160
|
project_root: Project root path
|
|
161
161
|
|
|
162
162
|
Returns:
|
|
163
|
-
Number of stories
|
|
163
|
+
Number of stories available for work
|
|
164
164
|
"""
|
|
165
165
|
# Import here to avoid circular imports
|
|
166
166
|
from pennyfarthing_scripts.sprint.loader import load_sprint
|
|
@@ -171,9 +171,11 @@ def get_backlog_count(project_root: Path) -> int:
|
|
|
171
171
|
|
|
172
172
|
count = 0
|
|
173
173
|
for epic in sprint["epics"]:
|
|
174
|
+
if not isinstance(epic, dict):
|
|
175
|
+
continue # Skip string refs (defensive)
|
|
174
176
|
for story in epic.get("stories", []):
|
|
175
177
|
status = story.get("status", "").lower()
|
|
176
|
-
if status in ("backlog", "ready"):
|
|
178
|
+
if status in ("backlog", "ready", "planning"):
|
|
177
179
|
count += 1
|
|
178
180
|
|
|
179
181
|
return count
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -18,7 +18,7 @@ def archive_story(
|
|
|
18
18
|
dry_run: bool = False,
|
|
19
19
|
apply: bool = False,
|
|
20
20
|
) -> dict[str, Any]:
|
|
21
|
-
"""Archive a completed story.
|
|
21
|
+
"""Archive a completed story to the sprint archive file.
|
|
22
22
|
|
|
23
23
|
Args:
|
|
24
24
|
story_id: Story ID to archive
|
|
@@ -29,6 +29,11 @@ def archive_story(
|
|
|
29
29
|
Returns:
|
|
30
30
|
Dict with success status and details
|
|
31
31
|
"""
|
|
32
|
+
import re
|
|
33
|
+
from datetime import date
|
|
34
|
+
|
|
35
|
+
import yaml
|
|
36
|
+
|
|
32
37
|
# Find the story
|
|
33
38
|
story = get_story_by_id(story_id)
|
|
34
39
|
if not story:
|
|
@@ -45,23 +50,75 @@ def archive_story(
|
|
|
45
50
|
"error": f"Story status is '{status}', expected 'done' or 'completed'",
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
root = get_project_root()
|
|
54
|
+
sprint_file = root / "sprint" / "current-sprint.yaml"
|
|
55
|
+
|
|
56
|
+
if not sprint_file.exists():
|
|
57
|
+
return {"success": False, "error": f"Sprint file not found: {sprint_file}"}
|
|
58
|
+
|
|
59
|
+
# Get sprint name for archive file
|
|
60
|
+
with open(sprint_file) as f:
|
|
61
|
+
sprint_data = yaml.safe_load(f.read())
|
|
62
|
+
|
|
63
|
+
sprint_name = sprint_data.get("sprint", {}).get("jira_sprint_name", "")
|
|
64
|
+
match = re.search(r"(\d{4})", sprint_name)
|
|
65
|
+
sprint_num = match.group(1) if match else "unknown"
|
|
66
|
+
archive_file = root / "sprint" / "archive" / f"sprint-{sprint_num}-completed.yaml"
|
|
67
|
+
|
|
68
|
+
# Find parent epic
|
|
69
|
+
epic_id = ""
|
|
70
|
+
for epic in sprint_data.get("epics", []):
|
|
71
|
+
if isinstance(epic, dict):
|
|
72
|
+
for s in epic.get("stories", []):
|
|
73
|
+
if s.get("id") == story_id:
|
|
74
|
+
epic_id = str(epic.get("id", ""))
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
completed_date = str(date.today())
|
|
78
|
+
|
|
48
79
|
if dry_run:
|
|
49
80
|
return {
|
|
50
81
|
"success": True,
|
|
51
82
|
"dry_run": True,
|
|
52
83
|
"story": story,
|
|
53
84
|
"pr_number": pr_number,
|
|
54
|
-
"message": f"Would archive {story_id}",
|
|
85
|
+
"message": f"Would archive {story_id} to {archive_file}",
|
|
55
86
|
}
|
|
56
87
|
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
88
|
+
# Append to archive file
|
|
89
|
+
if not archive_file.exists():
|
|
90
|
+
return {"success": False, "error": f"Archive file not found: {archive_file}"}
|
|
91
|
+
|
|
92
|
+
entry_lines = [
|
|
93
|
+
f" - id: {story_id}",
|
|
94
|
+
f" epic: {epic_id}",
|
|
95
|
+
f' title: "{story.get("title", "Unknown")}"',
|
|
96
|
+
f" points: {story.get('points', 0)}",
|
|
97
|
+
f" completed: {completed_date}",
|
|
98
|
+
]
|
|
99
|
+
if pr_number:
|
|
100
|
+
entry_lines.append(f" pr: {pr_number}")
|
|
101
|
+
|
|
102
|
+
with open(archive_file, "a") as f:
|
|
103
|
+
f.write("\n".join(entry_lines) + "\n")
|
|
104
|
+
|
|
105
|
+
msg = f"Archived {story_id} to {archive_file.name}"
|
|
106
|
+
|
|
107
|
+
# Remove from current sprint if --apply
|
|
108
|
+
if apply:
|
|
109
|
+
for epic in sprint_data.get("epics", []):
|
|
110
|
+
if isinstance(epic, dict):
|
|
111
|
+
epic["stories"] = [s for s in epic.get("stories", []) if s.get("id") != story_id]
|
|
112
|
+
|
|
113
|
+
from pennyfarthing_scripts.sprint.yaml_io import write_sprint
|
|
114
|
+
write_sprint(sprint_file, sprint_data)
|
|
115
|
+
msg += f" and removed from {sprint_file.name}"
|
|
116
|
+
|
|
60
117
|
return {
|
|
61
118
|
"success": True,
|
|
62
119
|
"story": story,
|
|
63
120
|
"pr_number": pr_number,
|
|
64
|
-
"message":
|
|
121
|
+
"message": msg,
|
|
65
122
|
}
|
|
66
123
|
|
|
67
124
|
|