@pennyfarthing/core 11.2.2 → 11.3.2
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 +3 -3
- package/package.json +1 -1
- package/packages/core/dist/cli/commands/doctor-legacy.test.js +2 -2
- package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -1
- package/packages/core/dist/cli/commands/doctor.d.ts +63 -0
- package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/doctor.js +280 -43
- package/packages/core/dist/cli/commands/doctor.js.map +1 -1
- package/packages/core/dist/cli/commands/init.d.ts +12 -0
- package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/init.js +45 -0
- package/packages/core/dist/cli/commands/init.js.map +1 -1
- package/packages/core/dist/cli/commands/pyproject-install.test.d.ts +19 -0
- package/packages/core/dist/cli/commands/pyproject-install.test.d.ts.map +1 -0
- package/packages/core/dist/cli/commands/pyproject-install.test.js +261 -0
- package/packages/core/dist/cli/commands/pyproject-install.test.js.map +1 -0
- package/packages/core/dist/cli/commands/update-consolidation.test.js +14 -6
- package/packages/core/dist/cli/commands/update-consolidation.test.js.map +1 -1
- package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/update.js +5 -1
- package/packages/core/dist/cli/commands/update.js.map +1 -1
- package/packages/core/dist/cli/index.js +2 -0
- package/packages/core/dist/cli/index.js.map +1 -1
- package/packages/core/dist/cli/utils/python.d.ts +1 -0
- package/packages/core/dist/cli/utils/python.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/python.js +22 -1
- package/packages/core/dist/cli/utils/python.js.map +1 -1
- package/packages/core/dist/cli/utils/settings-hook-migration.test.d.ts +17 -0
- package/packages/core/dist/cli/utils/settings-hook-migration.test.d.ts.map +1 -0
- package/packages/core/dist/cli/utils/settings-hook-migration.test.js +382 -0
- package/packages/core/dist/cli/utils/settings-hook-migration.test.js.map +1 -0
- package/packages/core/dist/cli/utils/settings-pf-wrapper.test.d.ts +16 -0
- package/packages/core/dist/cli/utils/settings-pf-wrapper.test.d.ts.map +1 -0
- package/packages/core/dist/cli/utils/settings-pf-wrapper.test.js +377 -0
- package/packages/core/dist/cli/utils/settings-pf-wrapper.test.js.map +1 -0
- package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/settings.js +15 -2
- package/packages/core/dist/cli/utils/settings.js.map +1 -1
- package/packages/core/dist/server/paths.d.ts.map +1 -1
- package/packages/core/dist/server/paths.js +6 -0
- package/packages/core/dist/server/paths.js.map +1 -1
- package/packages/core/dist/server/settings.d.ts.map +1 -1
- package/packages/core/dist/server/settings.js +5 -0
- package/packages/core/dist/server/settings.js.map +1 -1
- package/packages/core/dist/workflow/tandem-workflow-templates.test.js +7 -5
- package/packages/core/dist/workflow/tandem-workflow-templates.test.js.map +1 -1
- package/packages/core/dist/workflow/workflow-graph-validation.d.ts +65 -0
- package/packages/core/dist/workflow/workflow-graph-validation.d.ts.map +1 -0
- package/packages/core/dist/workflow/workflow-graph-validation.js +190 -0
- package/packages/core/dist/workflow/workflow-graph-validation.js.map +1 -0
- package/packages/core/dist/workflow/workflow-graph-validation.test.d.ts +18 -0
- package/packages/core/dist/workflow/workflow-graph-validation.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/workflow-graph-validation.test.js +706 -0
- package/packages/core/dist/workflow/workflow-graph-validation.test.js.map +1 -0
- package/packages/core/dist/workflow/workflow-migration.test.js +6 -5
- package/packages/core/dist/workflow/workflow-migration.test.js.map +1 -1
- package/pennyfarthing-dist/agents/dev.md +4 -2
- package/pennyfarthing-dist/agents/devops.md +2 -10
- package/pennyfarthing-dist/agents/reviewer-preflight.md +4 -5
- package/pennyfarthing-dist/agents/sm.md +4 -17
- package/pennyfarthing-dist/commands/pf-health-check.md +30 -11
- package/pennyfarthing-dist/gates/{confidence-sm.md → confidence.md} +16 -17
- package/pennyfarthing-dist/gates/dev-exit.md +75 -0
- package/pennyfarthing-dist/gates/merge-ready.md +49 -0
- package/pennyfarthing-dist/gates/release-ready.md +95 -0
- package/pennyfarthing-dist/gates/reviewer-preflight-check.md +90 -0
- package/pennyfarthing-dist/gates/sm-setup-exit.md +82 -0
- package/pennyfarthing-dist/guides/agent-behavior.md +88 -30
- package/pennyfarthing-dist/guides/gates.md +7 -2
- package/pennyfarthing-dist/scripts/lib/find-root.sh +5 -0
- package/pennyfarthing-dist/scripts/lib/run-pf.sh +7 -0
- package/pennyfarthing-dist/skills/pf-settings/skill.md +42 -0
- package/pennyfarthing-dist/skills/skill-registry.yaml +15 -0
- package/pennyfarthing-dist/templates/pyproject.toml +27 -0
- package/pennyfarthing-dist/workflows/bdd-tandem.yaml +7 -3
- package/pennyfarthing-dist/workflows/bdd.yaml +7 -3
- package/pennyfarthing-dist/workflows/installation-check/steps/step-01-foundation.md +77 -0
- package/pennyfarthing-dist/workflows/installation-check/steps/step-02-commands.md +82 -0
- package/pennyfarthing-dist/workflows/installation-check/steps/step-03-hooks.md +121 -0
- package/pennyfarthing-dist/workflows/installation-check/steps/step-04-scripts.md +83 -0
- package/pennyfarthing-dist/workflows/installation-check/steps/step-05-layout.md +81 -0
- package/pennyfarthing-dist/workflows/installation-check/steps/step-06-legacy.md +94 -0
- package/pennyfarthing-dist/workflows/installation-check/steps/step-07-tools.md +80 -0
- package/pennyfarthing-dist/workflows/installation-check/steps/step-08-summary.md +99 -0
- package/pennyfarthing-dist/workflows/installation-check/workflow.yaml +47 -0
- package/pennyfarthing-dist/workflows/tdd-tandem.yaml +7 -3
- package/pennyfarthing-dist/workflows/tdd.yaml +7 -3
- package/pennyfarthing-dist/workflows/trivial.yaml +7 -3
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/context.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/bc/__pycache__/split.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/cli.py +21 -0
- package/pennyfarthing_scripts/bc/focus.py +1 -0
- package/pennyfarthing_scripts/bc/split.py +52 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/audit_log_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__/context_meter_footer.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__/events.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/portrait_resolver.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/progress_panel.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/context_meter_footer.py +53 -3
- package/pennyfarthing_scripts/bikerack/tui.py +202 -8
- package/pennyfarthing_scripts/bmad/__init__.py +1 -0
- package/pennyfarthing_scripts/bmad/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bmad/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bmad/__pycache__/parser.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bmad/__pycache__/sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bmad/__pycache__/test_parser.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/bmad/__pycache__/test_sync.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/bmad/cli.py +197 -0
- package/pennyfarthing_scripts/bmad/importer.py +200 -0
- package/pennyfarthing_scripts/bmad/parser.py +233 -0
- package/pennyfarthing_scripts/bmad/sync.py +464 -0
- package/pennyfarthing_scripts/bmad/test_parser.py +253 -0
- package/pennyfarthing_scripts/bmad/test_sync.py +223 -0
- package/pennyfarthing_scripts/cli.py +10 -0
- package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/consultation/__pycache__/cli.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/hooks/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/bell_mode.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/context_breaker.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/context_warning.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/cyclist_pretooluse.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/pre_edit_check.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/reflector_check.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/schema_validation.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/session_stop.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/sprint_yaml_validation.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/statusline.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/settings/__init__.py +0 -0
- package/pennyfarthing_scripts/settings/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/settings/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/settings/__pycache__/settings.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/settings/cli.py +55 -0
- package/pennyfarthing_scripts/settings/settings.py +98 -0
- package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_confidence_sm_evaluation.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_confidence_sm_gate.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_resolve_gate_file_field.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_workflow_list_team.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/tests/test_confidence_sm_gate.py +17 -16
- package/pennyfarthing_scripts/tests/test_resolve_gate_file_field.py +45 -47
- package/pennyfarthing_scripts/tests/test_workflow_list_team.py +0 -4
- package/pennyfarthing_scripts/workflow/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/state.cpython-314.pyc +0 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bidirectional sync between BMAD markdown and PF sprint YAML.
|
|
3
|
+
|
|
4
|
+
Modeled on pennyfarthing_scripts/jira/bidirectional.py — same
|
|
5
|
+
SyncPlan/SyncChange/SyncResult pattern, adapted for BMAD's flat
|
|
6
|
+
markdown header format instead of a REST API.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Literal
|
|
15
|
+
|
|
16
|
+
from pennyfarthing_scripts.bmad.parser import (
|
|
17
|
+
discover_bmad_stories,
|
|
18
|
+
map_bmad_to_pf,
|
|
19
|
+
map_pf_to_bmad,
|
|
20
|
+
)
|
|
21
|
+
from pennyfarthing_scripts.common.config import get_project_root, load_pennyfarthing_config
|
|
22
|
+
from pennyfarthing_scripts.common.output import error, info, success, warn
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# =============================================================================
|
|
26
|
+
# Data Classes
|
|
27
|
+
# =============================================================================
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class BmadSyncChange:
|
|
32
|
+
"""A single sync change to apply."""
|
|
33
|
+
|
|
34
|
+
bmad_key: str
|
|
35
|
+
pf_id: str
|
|
36
|
+
field: Literal["status"]
|
|
37
|
+
action: Literal["update-pf", "update-bmad"]
|
|
38
|
+
pf_value: Any
|
|
39
|
+
bmad_value: Any
|
|
40
|
+
target_value: Any
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class BmadSyncPlan:
|
|
45
|
+
"""Result of comparing PF YAML and BMAD markdown."""
|
|
46
|
+
|
|
47
|
+
changes: list[BmadSyncChange] = field(default_factory=list)
|
|
48
|
+
pf_only: list[str] = field(default_factory=list)
|
|
49
|
+
bmad_only: list[str] = field(default_factory=list)
|
|
50
|
+
both: list[str] = field(default_factory=list)
|
|
51
|
+
conflicts: list[dict[str, Any]] = field(default_factory=list)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class BmadSyncResult:
|
|
56
|
+
"""Result of executing a sync plan."""
|
|
57
|
+
|
|
58
|
+
dry_run: bool
|
|
59
|
+
changes_planned: int
|
|
60
|
+
changes_applied: int
|
|
61
|
+
pf_modified: bool
|
|
62
|
+
bmad_modified: bool
|
|
63
|
+
new_stories_imported: int = 0
|
|
64
|
+
errors: list[str] = field(default_factory=list)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# =============================================================================
|
|
68
|
+
# Sync Plan Generation
|
|
69
|
+
# =============================================================================
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _collect_pf_stories(sprint_path: Path) -> list[dict[str, Any]]:
|
|
73
|
+
"""Load all PF stories that have a bmad_key field."""
|
|
74
|
+
from pennyfarthing_scripts.sprint.yaml_io import read_sprint
|
|
75
|
+
|
|
76
|
+
data = read_sprint(sprint_path)
|
|
77
|
+
stories: list[dict[str, Any]] = []
|
|
78
|
+
for epic in data.get("epics", []):
|
|
79
|
+
for story in epic.get("stories", []):
|
|
80
|
+
if story.get("bmad_key"):
|
|
81
|
+
stories.append(dict(story))
|
|
82
|
+
return stories
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def generate_sync_plan(
|
|
86
|
+
pf_stories: list[dict[str, Any]],
|
|
87
|
+
bmad_stories: list[dict[str, Any]],
|
|
88
|
+
*,
|
|
89
|
+
direction: Literal["pull", "push", "both"] = "both",
|
|
90
|
+
pf_wins: bool = True,
|
|
91
|
+
) -> BmadSyncPlan:
|
|
92
|
+
"""Compare PF and BMAD stories and build a sync plan.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
pf_stories: Stories from PF YAML (must have bmad_key field)
|
|
96
|
+
bmad_stories: Stories parsed from BMAD markdown
|
|
97
|
+
direction: "pull" (BMAD→PF), "push" (PF→BMAD), or "both"
|
|
98
|
+
pf_wins: If True, PF status wins on conflict (default for push)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
BmadSyncPlan with changes, conflicts, and set membership.
|
|
102
|
+
"""
|
|
103
|
+
plan = BmadSyncPlan()
|
|
104
|
+
|
|
105
|
+
# Build lookup maps keyed on bmad_key
|
|
106
|
+
pf_by_key: dict[str, dict] = {}
|
|
107
|
+
for story in pf_stories:
|
|
108
|
+
key = story.get("bmad_key", "")
|
|
109
|
+
if key:
|
|
110
|
+
pf_by_key[key] = story
|
|
111
|
+
|
|
112
|
+
bmad_by_key: dict[str, dict] = {}
|
|
113
|
+
for story in bmad_stories:
|
|
114
|
+
key = story.get("bmad_key", "")
|
|
115
|
+
if key:
|
|
116
|
+
bmad_by_key[key] = story
|
|
117
|
+
|
|
118
|
+
pf_keys = set(pf_by_key.keys())
|
|
119
|
+
bmad_keys = set(bmad_by_key.keys())
|
|
120
|
+
|
|
121
|
+
plan.pf_only = sorted(pf_keys - bmad_keys)
|
|
122
|
+
plan.bmad_only = sorted(bmad_keys - pf_keys)
|
|
123
|
+
plan.both = sorted(pf_keys & bmad_keys)
|
|
124
|
+
|
|
125
|
+
# Compare matched stories
|
|
126
|
+
for key in plan.both:
|
|
127
|
+
pf_story = pf_by_key[key]
|
|
128
|
+
bmad_story = bmad_by_key[key]
|
|
129
|
+
|
|
130
|
+
pf_status = pf_story.get("status", "planning")
|
|
131
|
+
bmad_status_raw = bmad_story.get("bmad_status", "draft")
|
|
132
|
+
bmad_status_as_pf = map_bmad_to_pf(bmad_status_raw)
|
|
133
|
+
|
|
134
|
+
if pf_status == bmad_status_as_pf:
|
|
135
|
+
continue # In sync
|
|
136
|
+
|
|
137
|
+
pf_id = pf_story.get("id", key)
|
|
138
|
+
|
|
139
|
+
if direction == "pull":
|
|
140
|
+
# BMAD → PF
|
|
141
|
+
plan.changes.append(
|
|
142
|
+
BmadSyncChange(
|
|
143
|
+
bmad_key=key,
|
|
144
|
+
pf_id=pf_id,
|
|
145
|
+
field="status",
|
|
146
|
+
action="update-pf",
|
|
147
|
+
pf_value=pf_status,
|
|
148
|
+
bmad_value=bmad_status_raw,
|
|
149
|
+
target_value=bmad_status_as_pf,
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
elif direction == "push":
|
|
153
|
+
# PF → BMAD
|
|
154
|
+
target_bmad = map_pf_to_bmad(pf_status)
|
|
155
|
+
plan.changes.append(
|
|
156
|
+
BmadSyncChange(
|
|
157
|
+
bmad_key=key,
|
|
158
|
+
pf_id=pf_id,
|
|
159
|
+
field="status",
|
|
160
|
+
action="update-bmad",
|
|
161
|
+
pf_value=pf_status,
|
|
162
|
+
bmad_value=bmad_status_raw,
|
|
163
|
+
target_value=target_bmad,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
else:
|
|
167
|
+
# Both directions — resolve by pf_wins flag
|
|
168
|
+
if pf_wins:
|
|
169
|
+
target_bmad = map_pf_to_bmad(pf_status)
|
|
170
|
+
plan.changes.append(
|
|
171
|
+
BmadSyncChange(
|
|
172
|
+
bmad_key=key,
|
|
173
|
+
pf_id=pf_id,
|
|
174
|
+
field="status",
|
|
175
|
+
action="update-bmad",
|
|
176
|
+
pf_value=pf_status,
|
|
177
|
+
bmad_value=bmad_status_raw,
|
|
178
|
+
target_value=target_bmad,
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
else:
|
|
182
|
+
plan.changes.append(
|
|
183
|
+
BmadSyncChange(
|
|
184
|
+
bmad_key=key,
|
|
185
|
+
pf_id=pf_id,
|
|
186
|
+
field="status",
|
|
187
|
+
action="update-pf",
|
|
188
|
+
pf_value=pf_status,
|
|
189
|
+
bmad_value=bmad_status_raw,
|
|
190
|
+
target_value=bmad_status_as_pf,
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return plan
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# =============================================================================
|
|
198
|
+
# Sync Plan Execution
|
|
199
|
+
# =============================================================================
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _update_bmad_file_status(bmad_path: str, new_status: str) -> bool:
|
|
203
|
+
"""Rewrite the Status: line in a BMAD markdown file.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
bmad_path: Absolute path to the .md file
|
|
207
|
+
new_status: New BMAD status string (e.g. "completed")
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
True if the file was modified.
|
|
211
|
+
"""
|
|
212
|
+
path = Path(bmad_path)
|
|
213
|
+
if not path.exists():
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
content = path.read_text()
|
|
217
|
+
new_content, count = re.subn(
|
|
218
|
+
r"^(Status:\s*)(.+)$",
|
|
219
|
+
rf"\g<1>{new_status}",
|
|
220
|
+
content,
|
|
221
|
+
count=1,
|
|
222
|
+
flags=re.MULTILINE,
|
|
223
|
+
)
|
|
224
|
+
if count == 0:
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
path.write_text(new_content)
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def execute_sync_plan(
|
|
232
|
+
plan: BmadSyncPlan,
|
|
233
|
+
*,
|
|
234
|
+
dry_run: bool = False,
|
|
235
|
+
sprint_path: Path | None = None,
|
|
236
|
+
bmad_root: Path | None = None,
|
|
237
|
+
import_new: bool = False,
|
|
238
|
+
repos: str = "axiathon",
|
|
239
|
+
) -> BmadSyncResult:
|
|
240
|
+
"""Execute a sync plan.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
plan: The sync plan to execute
|
|
244
|
+
dry_run: If True, report without applying
|
|
245
|
+
sprint_path: Path to PF sprint YAML
|
|
246
|
+
bmad_root: Path to BMAD _bmad-output/ root
|
|
247
|
+
import_new: If True, import bmad_only stories as new PF stories
|
|
248
|
+
repos: Default repos for new story imports
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
BmadSyncResult with counts and errors.
|
|
252
|
+
"""
|
|
253
|
+
result = BmadSyncResult(
|
|
254
|
+
dry_run=dry_run,
|
|
255
|
+
changes_planned=len(plan.changes),
|
|
256
|
+
changes_applied=0,
|
|
257
|
+
pf_modified=False,
|
|
258
|
+
bmad_modified=False,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if dry_run:
|
|
262
|
+
return result
|
|
263
|
+
|
|
264
|
+
# Apply PF updates (YAML)
|
|
265
|
+
pf_updates = [c for c in plan.changes if c.action == "update-pf"]
|
|
266
|
+
if pf_updates and sprint_path:
|
|
267
|
+
from pennyfarthing_scripts.sprint.story_update import update_story
|
|
268
|
+
|
|
269
|
+
for change in pf_updates:
|
|
270
|
+
update_result = update_story(
|
|
271
|
+
sprint_path,
|
|
272
|
+
change.pf_id,
|
|
273
|
+
status=change.target_value,
|
|
274
|
+
)
|
|
275
|
+
if update_result.get("success"):
|
|
276
|
+
result.changes_applied += 1
|
|
277
|
+
result.pf_modified = True
|
|
278
|
+
else:
|
|
279
|
+
result.errors.append(
|
|
280
|
+
f"{change.pf_id}: PF update failed — {update_result.get('error', 'unknown')}"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Apply BMAD updates (markdown files)
|
|
284
|
+
bmad_updates = [c for c in plan.changes if c.action == "update-bmad"]
|
|
285
|
+
if bmad_updates:
|
|
286
|
+
# Need bmad story data to find file paths
|
|
287
|
+
# Re-discover to get bmad_path for each key
|
|
288
|
+
bmad_paths: dict[str, str] = {}
|
|
289
|
+
if bmad_root:
|
|
290
|
+
config = load_pennyfarthing_config()
|
|
291
|
+
bmad_config = config.get("bmad", {})
|
|
292
|
+
story_subdir = bmad_config.get("story_dir", "implementation-artifacts")
|
|
293
|
+
from pennyfarthing_scripts.bmad.parser import discover_bmad_stories as _discover
|
|
294
|
+
|
|
295
|
+
all_bmad = _discover(bmad_root, story_dir=story_subdir)
|
|
296
|
+
bmad_paths = {s["bmad_key"]: s["bmad_path"] for s in all_bmad}
|
|
297
|
+
|
|
298
|
+
for change in bmad_updates:
|
|
299
|
+
file_path = bmad_paths.get(change.bmad_key)
|
|
300
|
+
if not file_path:
|
|
301
|
+
result.errors.append(f"{change.bmad_key}: BMAD file not found")
|
|
302
|
+
continue
|
|
303
|
+
if _update_bmad_file_status(file_path, change.target_value):
|
|
304
|
+
result.changes_applied += 1
|
|
305
|
+
result.bmad_modified = True
|
|
306
|
+
else:
|
|
307
|
+
result.errors.append(f"{change.bmad_key}: Failed to update Status line")
|
|
308
|
+
|
|
309
|
+
# Import new BMAD stories not yet in PF
|
|
310
|
+
if import_new and plan.bmad_only and sprint_path and bmad_root:
|
|
311
|
+
result.new_stories_imported = _import_new_stories(
|
|
312
|
+
plan.bmad_only, bmad_root, sprint_path, repos, result
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
return result
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _import_new_stories(
|
|
319
|
+
bmad_keys: list[str],
|
|
320
|
+
bmad_root: Path,
|
|
321
|
+
sprint_path: Path,
|
|
322
|
+
repos: str,
|
|
323
|
+
result: BmadSyncResult,
|
|
324
|
+
) -> int:
|
|
325
|
+
"""Import stories that exist in BMAD but not PF.
|
|
326
|
+
|
|
327
|
+
Returns count of successfully imported stories.
|
|
328
|
+
"""
|
|
329
|
+
from pennyfarthing_scripts.bmad.parser import discover_bmad_stories as _discover
|
|
330
|
+
from pennyfarthing_scripts.sprint.yaml_io import read_sprint, write_sprint
|
|
331
|
+
|
|
332
|
+
config = load_pennyfarthing_config()
|
|
333
|
+
bmad_config = config.get("bmad", {})
|
|
334
|
+
story_subdir = bmad_config.get("story_dir", "implementation-artifacts")
|
|
335
|
+
|
|
336
|
+
all_bmad = _discover(bmad_root, story_dir=story_subdir)
|
|
337
|
+
bmad_by_key = {s["bmad_key"]: s for s in all_bmad}
|
|
338
|
+
|
|
339
|
+
data = read_sprint(sprint_path)
|
|
340
|
+
imported = 0
|
|
341
|
+
|
|
342
|
+
for key in bmad_keys:
|
|
343
|
+
bmad_story = bmad_by_key.get(key)
|
|
344
|
+
if not bmad_story:
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
epic_num = int(bmad_story["epic_num"])
|
|
348
|
+
|
|
349
|
+
# Find or create the epic in sprint data
|
|
350
|
+
target_epic = None
|
|
351
|
+
for epic in data.get("epics", []):
|
|
352
|
+
epic_id = str(epic.get("id", "")).replace("epic-", "")
|
|
353
|
+
if epic_id == str(epic_num):
|
|
354
|
+
target_epic = epic
|
|
355
|
+
break
|
|
356
|
+
|
|
357
|
+
if target_epic is None:
|
|
358
|
+
# Create new epic shard
|
|
359
|
+
target_epic = {
|
|
360
|
+
"id": str(epic_num),
|
|
361
|
+
"title": f"Epic {epic_num}",
|
|
362
|
+
"status": "planning",
|
|
363
|
+
"priority": "P1",
|
|
364
|
+
"marker": "bmad",
|
|
365
|
+
"repos": repos,
|
|
366
|
+
"stories": [],
|
|
367
|
+
}
|
|
368
|
+
data.setdefault("epics", []).append(target_epic)
|
|
369
|
+
|
|
370
|
+
# Add story
|
|
371
|
+
pf_story = {
|
|
372
|
+
"id": bmad_story["id"],
|
|
373
|
+
"title": bmad_story["title"],
|
|
374
|
+
"points": bmad_story.get("points", 3),
|
|
375
|
+
"priority": bmad_story.get("priority", "P1"),
|
|
376
|
+
"status": bmad_story["status"],
|
|
377
|
+
"repos": repos,
|
|
378
|
+
"workflow": bmad_story.get("workflow", "tdd"),
|
|
379
|
+
"bmad_key": key,
|
|
380
|
+
}
|
|
381
|
+
target_epic.setdefault("stories", []).append(pf_story)
|
|
382
|
+
imported += 1
|
|
383
|
+
|
|
384
|
+
if imported > 0:
|
|
385
|
+
write_sprint(sprint_path, data)
|
|
386
|
+
result.pf_modified = True
|
|
387
|
+
|
|
388
|
+
return imported
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# =============================================================================
|
|
392
|
+
# Formatting
|
|
393
|
+
# =============================================================================
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def format_sync_plan(plan: BmadSyncPlan) -> str:
|
|
397
|
+
"""Format a sync plan for human-readable display.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
plan: The plan to format
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Formatted string.
|
|
404
|
+
"""
|
|
405
|
+
lines: list[str] = []
|
|
406
|
+
|
|
407
|
+
lines.append(f"Matched: {len(plan.both)} | PF-only: {len(plan.pf_only)} | BMAD-only: {len(plan.bmad_only)}")
|
|
408
|
+
lines.append("")
|
|
409
|
+
|
|
410
|
+
if plan.changes:
|
|
411
|
+
lines.append(f"Changes ({len(plan.changes)}):")
|
|
412
|
+
for c in plan.changes:
|
|
413
|
+
arrow = "BMAD→PF" if c.action == "update-pf" else "PF→BMAD"
|
|
414
|
+
lines.append(
|
|
415
|
+
f" {c.pf_id} ({c.bmad_key}): {c.field} {arrow} "
|
|
416
|
+
f"{c.pf_value!r} / {c.bmad_value!r} → {c.target_value!r}"
|
|
417
|
+
)
|
|
418
|
+
lines.append("")
|
|
419
|
+
|
|
420
|
+
if plan.bmad_only:
|
|
421
|
+
lines.append(f"New in BMAD ({len(plan.bmad_only)}):")
|
|
422
|
+
for key in plan.bmad_only:
|
|
423
|
+
lines.append(f" {key}")
|
|
424
|
+
lines.append("")
|
|
425
|
+
|
|
426
|
+
if plan.pf_only:
|
|
427
|
+
lines.append(f"PF-only ({len(plan.pf_only)}):")
|
|
428
|
+
for key in plan.pf_only:
|
|
429
|
+
lines.append(f" {key}")
|
|
430
|
+
lines.append("")
|
|
431
|
+
|
|
432
|
+
if not plan.changes and not plan.bmad_only and not plan.pf_only:
|
|
433
|
+
lines.append("Everything is in sync.")
|
|
434
|
+
|
|
435
|
+
return "\n".join(lines)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# =============================================================================
|
|
439
|
+
# Drift Report
|
|
440
|
+
# =============================================================================
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def drift_report(
|
|
444
|
+
sprint_path: Path,
|
|
445
|
+
bmad_root: Path,
|
|
446
|
+
) -> str:
|
|
447
|
+
"""Generate a drift report showing what's out of sync.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
sprint_path: Path to PF sprint YAML
|
|
451
|
+
bmad_root: Path to BMAD _bmad-output/ root
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
Formatted drift report string.
|
|
455
|
+
"""
|
|
456
|
+
config = load_pennyfarthing_config()
|
|
457
|
+
bmad_config = config.get("bmad", {})
|
|
458
|
+
story_subdir = bmad_config.get("story_dir", "implementation-artifacts")
|
|
459
|
+
|
|
460
|
+
pf_stories = _collect_pf_stories(sprint_path)
|
|
461
|
+
bmad_stories = discover_bmad_stories(bmad_root, story_dir=story_subdir)
|
|
462
|
+
|
|
463
|
+
plan = generate_sync_plan(pf_stories, bmad_stories, direction="both")
|
|
464
|
+
return format_sync_plan(plan)
|