@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,253 @@
|
|
|
1
|
+
"""Tests for BMAD markdown parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from pennyfarthing_scripts.bmad.parser import (
|
|
11
|
+
BMAD_TO_PF_STATUS,
|
|
12
|
+
PF_TO_BMAD_STATUS,
|
|
13
|
+
discover_bmad_stories,
|
|
14
|
+
map_bmad_to_pf,
|
|
15
|
+
map_pf_to_bmad,
|
|
16
|
+
parse_bmad_epic,
|
|
17
|
+
parse_bmad_story,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# =============================================================================
|
|
21
|
+
# Sample BMAD content
|
|
22
|
+
# =============================================================================
|
|
23
|
+
|
|
24
|
+
SAMPLE_STORY = """\
|
|
25
|
+
# Story 2.1: OCSF Event Schema
|
|
26
|
+
|
|
27
|
+
Status: ready-for-dev
|
|
28
|
+
Story-Key: 2-1-ocsf-event-schema
|
|
29
|
+
Jira: DPGD-24 / DPGD-35
|
|
30
|
+
Epic: 2 - Core Event Ingestion & Storage
|
|
31
|
+
Date: 2026-02-17
|
|
32
|
+
|
|
33
|
+
## Story
|
|
34
|
+
|
|
35
|
+
As a **security engineer**,
|
|
36
|
+
I want **a standardized event schema based on OCSF**,
|
|
37
|
+
So that **all ingested events have consistent structure**.
|
|
38
|
+
|
|
39
|
+
## Acceptance Criteria
|
|
40
|
+
|
|
41
|
+
**AC-1: Schema Validation**
|
|
42
|
+
**Given** an incoming event
|
|
43
|
+
**When** it is ingested
|
|
44
|
+
**Then** it conforms to the OCSF schema
|
|
45
|
+
|
|
46
|
+
## Tasks / Subtasks
|
|
47
|
+
|
|
48
|
+
- [ ] Task 1: Define schema (AC: #1)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
SAMPLE_STORY_COMPLETED = """\
|
|
52
|
+
# Story 1.1: Initialize Cargo Workspace
|
|
53
|
+
|
|
54
|
+
Status: completed
|
|
55
|
+
Story-Key: 1-1-initialize-cargo-workspace
|
|
56
|
+
Jira: DPGD-10 / DPGD-15
|
|
57
|
+
Epic: 1 - Project Foundation & Developer Experience
|
|
58
|
+
Date: 2026-02-17
|
|
59
|
+
|
|
60
|
+
## Story
|
|
61
|
+
|
|
62
|
+
As a **developer**,
|
|
63
|
+
I want **a properly initialized Cargo workspace**,
|
|
64
|
+
So that **I can start developing**.
|
|
65
|
+
|
|
66
|
+
## Acceptance Criteria
|
|
67
|
+
|
|
68
|
+
**AC-1: Workspace Initialized**
|
|
69
|
+
**Given** a fresh clone
|
|
70
|
+
**When** I run cargo build
|
|
71
|
+
**Then** it succeeds
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
SAMPLE_EPIC = """\
|
|
75
|
+
---
|
|
76
|
+
epicNumber: 2
|
|
77
|
+
title: "Core Event Ingestion & Storage"
|
|
78
|
+
phase: "MVP"
|
|
79
|
+
status: "draft"
|
|
80
|
+
storyCount: 15
|
|
81
|
+
frsAddressed: ["FR25-FR40"]
|
|
82
|
+
nfrsAddressed: ["NFR10-NFR15"]
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
# Epic 2: Core Event Ingestion & Storage
|
|
86
|
+
|
|
87
|
+
## Epic Goal
|
|
88
|
+
|
|
89
|
+
Enable high-throughput event ingestion with columnar storage.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# =============================================================================
|
|
94
|
+
# Status Mapping
|
|
95
|
+
# =============================================================================
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TestStatusMapping:
|
|
99
|
+
def test_bmad_to_pf_all_values(self):
|
|
100
|
+
assert map_bmad_to_pf("draft") == "planning"
|
|
101
|
+
assert map_bmad_to_pf("ready-for-dev") == "ready"
|
|
102
|
+
assert map_bmad_to_pf("in-progress") == "in_progress"
|
|
103
|
+
assert map_bmad_to_pf("in-review") == "in_progress"
|
|
104
|
+
assert map_bmad_to_pf("completed") == "done"
|
|
105
|
+
assert map_bmad_to_pf("blocked") == "backlog"
|
|
106
|
+
|
|
107
|
+
def test_bmad_to_pf_unknown_defaults_to_planning(self):
|
|
108
|
+
assert map_bmad_to_pf("unknown-status") == "planning"
|
|
109
|
+
|
|
110
|
+
def test_bmad_to_pf_case_insensitive(self):
|
|
111
|
+
assert map_bmad_to_pf("Ready-For-Dev") == "ready"
|
|
112
|
+
assert map_bmad_to_pf("COMPLETED") == "done"
|
|
113
|
+
|
|
114
|
+
def test_pf_to_bmad_all_values(self):
|
|
115
|
+
assert map_pf_to_bmad("planning") == "draft"
|
|
116
|
+
assert map_pf_to_bmad("ready") == "ready-for-dev"
|
|
117
|
+
assert map_pf_to_bmad("in_progress") == "in-progress"
|
|
118
|
+
assert map_pf_to_bmad("done") == "completed"
|
|
119
|
+
assert map_pf_to_bmad("backlog") == "blocked"
|
|
120
|
+
assert map_pf_to_bmad("canceled") == "completed"
|
|
121
|
+
|
|
122
|
+
def test_pf_to_bmad_unknown_defaults_to_draft(self):
|
|
123
|
+
assert map_pf_to_bmad("unknown") == "draft"
|
|
124
|
+
|
|
125
|
+
def test_roundtrip_bmad_through_pf(self):
|
|
126
|
+
"""BMAD → PF → BMAD should preserve status (except in-review → in-progress → in-progress)."""
|
|
127
|
+
for bmad_status, pf_status in BMAD_TO_PF_STATUS.items():
|
|
128
|
+
roundtrip = map_pf_to_bmad(pf_status)
|
|
129
|
+
# in-review maps to in_progress which maps back to in-progress (not in-review)
|
|
130
|
+
if bmad_status == "in-review":
|
|
131
|
+
assert roundtrip == "in-progress"
|
|
132
|
+
else:
|
|
133
|
+
assert roundtrip == bmad_status, f"Roundtrip failed for {bmad_status}"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# =============================================================================
|
|
137
|
+
# Story Parsing
|
|
138
|
+
# =============================================================================
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TestParseStory:
|
|
142
|
+
def test_parse_basic_story(self, tmp_path: Path):
|
|
143
|
+
story_file = tmp_path / "2-1-ocsf-event-schema.md"
|
|
144
|
+
story_file.write_text(SAMPLE_STORY)
|
|
145
|
+
|
|
146
|
+
result = parse_bmad_story(story_file)
|
|
147
|
+
|
|
148
|
+
assert result["id"] == "2-1"
|
|
149
|
+
assert result["title"] == "OCSF Event Schema"
|
|
150
|
+
assert result["status"] == "ready"
|
|
151
|
+
assert result["bmad_key"] == "2-1-ocsf-event-schema"
|
|
152
|
+
assert result["bmad_status"] == "ready-for-dev"
|
|
153
|
+
assert result["jira"] == "DPGD-24 / DPGD-35"
|
|
154
|
+
assert result["epic_num"] == "2"
|
|
155
|
+
assert result["points"] == 3
|
|
156
|
+
assert "AC-1" in result["acceptance_criteria"]
|
|
157
|
+
|
|
158
|
+
def test_parse_completed_story(self, tmp_path: Path):
|
|
159
|
+
story_file = tmp_path / "1-1-initialize-cargo-workspace.md"
|
|
160
|
+
story_file.write_text(SAMPLE_STORY_COMPLETED)
|
|
161
|
+
|
|
162
|
+
result = parse_bmad_story(story_file)
|
|
163
|
+
|
|
164
|
+
assert result["id"] == "1-1"
|
|
165
|
+
assert result["status"] == "done"
|
|
166
|
+
assert result["bmad_status"] == "completed"
|
|
167
|
+
assert result["bmad_key"] == "1-1-initialize-cargo-workspace"
|
|
168
|
+
|
|
169
|
+
def test_parse_story_with_missing_fields(self, tmp_path: Path):
|
|
170
|
+
minimal = "# Story 5.3: Retry Logic\n\nSome content\n"
|
|
171
|
+
story_file = tmp_path / "5-3-retry-logic.md"
|
|
172
|
+
story_file.write_text(minimal)
|
|
173
|
+
|
|
174
|
+
result = parse_bmad_story(story_file)
|
|
175
|
+
|
|
176
|
+
# Should still parse with defaults
|
|
177
|
+
assert result["status"] == "planning"
|
|
178
|
+
assert result["bmad_status"] == "draft"
|
|
179
|
+
assert result["points"] == 3
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# =============================================================================
|
|
183
|
+
# Epic Parsing
|
|
184
|
+
# =============================================================================
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class TestParseEpic:
|
|
188
|
+
def test_parse_epic_with_frontmatter(self, tmp_path: Path):
|
|
189
|
+
epic_file = tmp_path / "epic-02-event-ingestion.md"
|
|
190
|
+
epic_file.write_text(SAMPLE_EPIC)
|
|
191
|
+
|
|
192
|
+
result = parse_bmad_epic(epic_file)
|
|
193
|
+
|
|
194
|
+
assert result["epicNumber"] == 2
|
|
195
|
+
assert result["title"] == "Core Event Ingestion & Storage"
|
|
196
|
+
assert result["phase"] == "MVP"
|
|
197
|
+
assert result["status"] == "draft"
|
|
198
|
+
assert result["storyCount"] == 15
|
|
199
|
+
|
|
200
|
+
def test_parse_epic_without_frontmatter(self, tmp_path: Path):
|
|
201
|
+
epic_file = tmp_path / "epic-99-something.md"
|
|
202
|
+
epic_file.write_text("# Epic 99: Something\n\nNo frontmatter here.\n")
|
|
203
|
+
|
|
204
|
+
result = parse_bmad_epic(epic_file)
|
|
205
|
+
|
|
206
|
+
assert result["epicNumber"] == 0
|
|
207
|
+
assert result["status"] == "draft"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# =============================================================================
|
|
211
|
+
# Discovery
|
|
212
|
+
# =============================================================================
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class TestDiscovery:
|
|
216
|
+
def test_discover_stories(self, tmp_path: Path):
|
|
217
|
+
impl = tmp_path / "implementation-artifacts"
|
|
218
|
+
impl.mkdir()
|
|
219
|
+
|
|
220
|
+
(impl / "1-1-workspace.md").write_text(SAMPLE_STORY_COMPLETED)
|
|
221
|
+
(impl / "2-1-schema.md").write_text(SAMPLE_STORY)
|
|
222
|
+
# Meta files should be skipped
|
|
223
|
+
(impl / "0-1-bmad-lifecycle.md").write_text("# Meta file\n")
|
|
224
|
+
# Non-matching files should be skipped
|
|
225
|
+
(impl / "readme.md").write_text("# Readme\n")
|
|
226
|
+
|
|
227
|
+
stories = discover_bmad_stories(tmp_path)
|
|
228
|
+
|
|
229
|
+
assert len(stories) == 2
|
|
230
|
+
assert stories[0]["id"] == "1-1"
|
|
231
|
+
assert stories[1]["id"] == "2-1"
|
|
232
|
+
|
|
233
|
+
def test_discover_stories_empty_dir(self, tmp_path: Path):
|
|
234
|
+
stories = discover_bmad_stories(tmp_path)
|
|
235
|
+
assert stories == []
|
|
236
|
+
|
|
237
|
+
def test_discover_epics(self, tmp_path: Path):
|
|
238
|
+
epics_dir = tmp_path / "planning-artifacts" / "epics"
|
|
239
|
+
epics_dir.mkdir(parents=True)
|
|
240
|
+
|
|
241
|
+
(epics_dir / "epic-01-foundation.md").write_text(
|
|
242
|
+
"---\nepicNumber: 1\ntitle: Foundation\nphase: MVP\nstatus: draft\nstoryCount: 5\n---\n"
|
|
243
|
+
)
|
|
244
|
+
(epics_dir / "epic-02-ingestion.md").write_text(SAMPLE_EPIC)
|
|
245
|
+
(epics_dir / "index.md").write_text("# Index\n")
|
|
246
|
+
|
|
247
|
+
from pennyfarthing_scripts.bmad.parser import discover_bmad_epics
|
|
248
|
+
|
|
249
|
+
epics = discover_bmad_epics(tmp_path)
|
|
250
|
+
|
|
251
|
+
assert len(epics) == 2
|
|
252
|
+
assert epics[0]["epicNumber"] == 1
|
|
253
|
+
assert epics[1]["epicNumber"] == 2
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Tests for BMAD sync engine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from pennyfarthing_scripts.bmad.sync import (
|
|
10
|
+
BmadSyncChange,
|
|
11
|
+
BmadSyncPlan,
|
|
12
|
+
_update_bmad_file_status,
|
|
13
|
+
format_sync_plan,
|
|
14
|
+
generate_sync_plan,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# =============================================================================
|
|
19
|
+
# Fixtures
|
|
20
|
+
# =============================================================================
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _pf_story(story_id: str, bmad_key: str, status: str = "ready") -> dict:
|
|
24
|
+
return {
|
|
25
|
+
"id": story_id,
|
|
26
|
+
"title": f"Story {story_id}",
|
|
27
|
+
"status": status,
|
|
28
|
+
"bmad_key": bmad_key,
|
|
29
|
+
"points": 3,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _bmad_story(bmad_key: str, bmad_status: str = "ready-for-dev") -> dict:
|
|
34
|
+
from pennyfarthing_scripts.bmad.parser import map_bmad_to_pf
|
|
35
|
+
|
|
36
|
+
parts = bmad_key.split("-", 2)
|
|
37
|
+
return {
|
|
38
|
+
"id": f"{parts[0]}-{parts[1]}",
|
|
39
|
+
"title": f"Story {bmad_key}",
|
|
40
|
+
"status": map_bmad_to_pf(bmad_status),
|
|
41
|
+
"bmad_key": bmad_key,
|
|
42
|
+
"bmad_status": bmad_status,
|
|
43
|
+
"bmad_path": f"/fake/{bmad_key}.md",
|
|
44
|
+
"epic_num": parts[0],
|
|
45
|
+
"points": 3,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# =============================================================================
|
|
50
|
+
# Sync Plan Generation
|
|
51
|
+
# =============================================================================
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestGenerateSyncPlan:
|
|
55
|
+
def test_all_in_sync(self):
|
|
56
|
+
pf = [_pf_story("1-1", "1-1-workspace", "ready")]
|
|
57
|
+
bmad = [_bmad_story("1-1-workspace", "ready-for-dev")]
|
|
58
|
+
|
|
59
|
+
plan = generate_sync_plan(pf, bmad, direction="both")
|
|
60
|
+
|
|
61
|
+
assert plan.both == ["1-1-workspace"]
|
|
62
|
+
assert plan.changes == []
|
|
63
|
+
assert plan.pf_only == []
|
|
64
|
+
assert plan.bmad_only == []
|
|
65
|
+
|
|
66
|
+
def test_pull_detects_bmad_change(self):
|
|
67
|
+
pf = [_pf_story("1-1", "1-1-workspace", "ready")]
|
|
68
|
+
bmad = [_bmad_story("1-1-workspace", "completed")]
|
|
69
|
+
|
|
70
|
+
plan = generate_sync_plan(pf, bmad, direction="pull")
|
|
71
|
+
|
|
72
|
+
assert len(plan.changes) == 1
|
|
73
|
+
change = plan.changes[0]
|
|
74
|
+
assert change.action == "update-pf"
|
|
75
|
+
assert change.pf_value == "ready"
|
|
76
|
+
assert change.bmad_value == "completed"
|
|
77
|
+
assert change.target_value == "done"
|
|
78
|
+
|
|
79
|
+
def test_push_detects_pf_change(self):
|
|
80
|
+
pf = [_pf_story("2-1", "2-1-schema", "done")]
|
|
81
|
+
bmad = [_bmad_story("2-1-schema", "ready-for-dev")]
|
|
82
|
+
|
|
83
|
+
plan = generate_sync_plan(pf, bmad, direction="push")
|
|
84
|
+
|
|
85
|
+
assert len(plan.changes) == 1
|
|
86
|
+
change = plan.changes[0]
|
|
87
|
+
assert change.action == "update-bmad"
|
|
88
|
+
assert change.target_value == "completed"
|
|
89
|
+
|
|
90
|
+
def test_both_pf_wins(self):
|
|
91
|
+
pf = [_pf_story("1-1", "1-1-workspace", "in_progress")]
|
|
92
|
+
bmad = [_bmad_story("1-1-workspace", "ready-for-dev")]
|
|
93
|
+
|
|
94
|
+
plan = generate_sync_plan(pf, bmad, direction="both", pf_wins=True)
|
|
95
|
+
|
|
96
|
+
assert len(plan.changes) == 1
|
|
97
|
+
assert plan.changes[0].action == "update-bmad"
|
|
98
|
+
assert plan.changes[0].target_value == "in-progress"
|
|
99
|
+
|
|
100
|
+
def test_both_bmad_wins(self):
|
|
101
|
+
pf = [_pf_story("1-1", "1-1-workspace", "in_progress")]
|
|
102
|
+
bmad = [_bmad_story("1-1-workspace", "completed")]
|
|
103
|
+
|
|
104
|
+
plan = generate_sync_plan(pf, bmad, direction="both", pf_wins=False)
|
|
105
|
+
|
|
106
|
+
assert len(plan.changes) == 1
|
|
107
|
+
assert plan.changes[0].action == "update-pf"
|
|
108
|
+
assert plan.changes[0].target_value == "done"
|
|
109
|
+
|
|
110
|
+
def test_pf_only_stories(self):
|
|
111
|
+
pf = [_pf_story("1-1", "1-1-workspace"), _pf_story("9-9", "9-9-custom")]
|
|
112
|
+
bmad = [_bmad_story("1-1-workspace")]
|
|
113
|
+
|
|
114
|
+
plan = generate_sync_plan(pf, bmad, direction="both")
|
|
115
|
+
|
|
116
|
+
assert plan.pf_only == ["9-9-custom"]
|
|
117
|
+
assert plan.both == ["1-1-workspace"]
|
|
118
|
+
|
|
119
|
+
def test_bmad_only_stories(self):
|
|
120
|
+
pf = [_pf_story("1-1", "1-1-workspace")]
|
|
121
|
+
bmad = [_bmad_story("1-1-workspace"), _bmad_story("3-2-rule-parser")]
|
|
122
|
+
|
|
123
|
+
plan = generate_sync_plan(pf, bmad, direction="both")
|
|
124
|
+
|
|
125
|
+
assert plan.bmad_only == ["3-2-rule-parser"]
|
|
126
|
+
|
|
127
|
+
def test_multiple_changes(self):
|
|
128
|
+
pf = [
|
|
129
|
+
_pf_story("1-1", "1-1-workspace", "done"),
|
|
130
|
+
_pf_story("2-1", "2-1-schema", "in_progress"),
|
|
131
|
+
]
|
|
132
|
+
bmad = [
|
|
133
|
+
_bmad_story("1-1-workspace", "ready-for-dev"),
|
|
134
|
+
_bmad_story("2-1-schema", "ready-for-dev"),
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
plan = generate_sync_plan(pf, bmad, direction="push")
|
|
138
|
+
|
|
139
|
+
assert len(plan.changes) == 2
|
|
140
|
+
|
|
141
|
+
def test_empty_inputs(self):
|
|
142
|
+
plan = generate_sync_plan([], [], direction="both")
|
|
143
|
+
|
|
144
|
+
assert plan.changes == []
|
|
145
|
+
assert plan.both == []
|
|
146
|
+
assert plan.pf_only == []
|
|
147
|
+
assert plan.bmad_only == []
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# =============================================================================
|
|
151
|
+
# BMAD File Update
|
|
152
|
+
# =============================================================================
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class TestUpdateBmadFile:
|
|
156
|
+
def test_update_status_line(self, tmp_path: Path):
|
|
157
|
+
md_file = tmp_path / "1-1-workspace.md"
|
|
158
|
+
md_file.write_text(
|
|
159
|
+
"# Story 1.1: Workspace\n\n"
|
|
160
|
+
"Status: ready-for-dev\n"
|
|
161
|
+
"Story-Key: 1-1-workspace\n"
|
|
162
|
+
"Jira: DPGD-10 / DPGD-15\n\n"
|
|
163
|
+
"## Story\n\nContent here.\n"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
result = _update_bmad_file_status(str(md_file), "completed")
|
|
167
|
+
|
|
168
|
+
assert result is True
|
|
169
|
+
content = md_file.read_text()
|
|
170
|
+
assert "Status: completed" in content
|
|
171
|
+
assert "ready-for-dev" not in content
|
|
172
|
+
# Other lines preserved
|
|
173
|
+
assert "Story-Key: 1-1-workspace" in content
|
|
174
|
+
assert "Jira: DPGD-10 / DPGD-15" in content
|
|
175
|
+
|
|
176
|
+
def test_update_nonexistent_file(self, tmp_path: Path):
|
|
177
|
+
result = _update_bmad_file_status(str(tmp_path / "missing.md"), "completed")
|
|
178
|
+
assert result is False
|
|
179
|
+
|
|
180
|
+
def test_update_file_without_status_line(self, tmp_path: Path):
|
|
181
|
+
md_file = tmp_path / "no-status.md"
|
|
182
|
+
md_file.write_text("# No status\n\nJust content.\n")
|
|
183
|
+
|
|
184
|
+
result = _update_bmad_file_status(str(md_file), "completed")
|
|
185
|
+
assert result is False
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# =============================================================================
|
|
189
|
+
# Formatting
|
|
190
|
+
# =============================================================================
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class TestFormatSyncPlan:
|
|
194
|
+
def test_format_empty_plan(self):
|
|
195
|
+
plan = BmadSyncPlan()
|
|
196
|
+
output = format_sync_plan(plan)
|
|
197
|
+
assert "Everything is in sync" in output
|
|
198
|
+
|
|
199
|
+
def test_format_with_changes(self):
|
|
200
|
+
plan = BmadSyncPlan(
|
|
201
|
+
changes=[
|
|
202
|
+
BmadSyncChange(
|
|
203
|
+
bmad_key="1-1-workspace",
|
|
204
|
+
pf_id="1-1",
|
|
205
|
+
field="status",
|
|
206
|
+
action="update-pf",
|
|
207
|
+
pf_value="ready",
|
|
208
|
+
bmad_value="completed",
|
|
209
|
+
target_value="done",
|
|
210
|
+
)
|
|
211
|
+
],
|
|
212
|
+
both=["1-1-workspace"],
|
|
213
|
+
)
|
|
214
|
+
output = format_sync_plan(plan)
|
|
215
|
+
assert "Changes (1)" in output
|
|
216
|
+
assert "BMAD→PF" in output
|
|
217
|
+
assert "1-1-workspace" in output
|
|
218
|
+
|
|
219
|
+
def test_format_with_new_bmad(self):
|
|
220
|
+
plan = BmadSyncPlan(bmad_only=["3-2-rule-parser", "3-3-rule-engine"])
|
|
221
|
+
output = format_sync_plan(plan)
|
|
222
|
+
assert "New in BMAD (2)" in output
|
|
223
|
+
assert "3-2-rule-parser" in output
|
|
@@ -90,6 +90,11 @@ from pennyfarthing_scripts.jira.cli import jira # noqa: E402
|
|
|
90
90
|
|
|
91
91
|
cli.add_command(jira)
|
|
92
92
|
|
|
93
|
+
# Import and register bmad group
|
|
94
|
+
from pennyfarthing_scripts.bmad.cli import bmad # noqa: E402
|
|
95
|
+
|
|
96
|
+
cli.add_command(bmad)
|
|
97
|
+
|
|
93
98
|
# Import and register theme group
|
|
94
99
|
from pennyfarthing_scripts.theme.cli import theme # noqa: E402
|
|
95
100
|
|
|
@@ -147,6 +152,11 @@ from pennyfarthing_scripts.hooks.cli import hooks # noqa: E402
|
|
|
147
152
|
|
|
148
153
|
cli.add_command(hooks)
|
|
149
154
|
|
|
155
|
+
# Import and register settings group
|
|
156
|
+
from pennyfarthing_scripts.settings.cli import settings # noqa: E402
|
|
157
|
+
|
|
158
|
+
cli.add_command(settings)
|
|
159
|
+
|
|
150
160
|
|
|
151
161
|
@cli.group()
|
|
152
162
|
def agent():
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Settings CLI group — view and manage .pennyfarthing/config.local.yaml.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
pf settings show # Pretty-print interesting settings
|
|
6
|
+
pf settings get <key> # Get value by dot-path
|
|
7
|
+
pf settings set <key> <value> # Set value by dot-path
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group()
|
|
14
|
+
def settings():
|
|
15
|
+
"""View and manage .pennyfarthing/config.local.yaml settings."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@settings.command()
|
|
20
|
+
def show():
|
|
21
|
+
"""Pretty-print all interesting settings."""
|
|
22
|
+
from pennyfarthing_scripts.settings.settings import show_settings
|
|
23
|
+
|
|
24
|
+
click.echo(show_settings())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@settings.command()
|
|
28
|
+
@click.argument("key")
|
|
29
|
+
def get(key: str):
|
|
30
|
+
"""Get a setting value by dot-path (e.g. workflow.relay_mode)."""
|
|
31
|
+
from pennyfarthing_scripts.settings.settings import get_setting
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
value = get_setting(key)
|
|
35
|
+
except KeyError:
|
|
36
|
+
click.echo(f"Key not found: {key}", err=True)
|
|
37
|
+
raise SystemExit(1) from None
|
|
38
|
+
|
|
39
|
+
if isinstance(value, dict):
|
|
40
|
+
import yaml
|
|
41
|
+
|
|
42
|
+
click.echo(yaml.dump(value, default_flow_style=False, sort_keys=False).rstrip())
|
|
43
|
+
else:
|
|
44
|
+
click.echo(value)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@settings.command()
|
|
48
|
+
@click.argument("key")
|
|
49
|
+
@click.argument("value")
|
|
50
|
+
def set(key: str, value: str):
|
|
51
|
+
"""Set a setting value by dot-path (e.g. workflow.bell_mode true)."""
|
|
52
|
+
from pennyfarthing_scripts.settings.settings import set_setting
|
|
53
|
+
|
|
54
|
+
set_setting(key, value)
|
|
55
|
+
click.echo(f"{key} = {value}")
|