@pennyfarthing/core 8.0.2 → 8.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/package.json +1 -1
- package/pennyfarthing-dist/agents/reviewer.md +21 -4
- package/pennyfarthing-dist/agents/sm.md +10 -0
- package/pennyfarthing-dist/commands/architect.md +1 -1
- package/pennyfarthing-dist/commands/dev.md +1 -1
- package/pennyfarthing-dist/commands/devops.md +1 -1
- package/pennyfarthing-dist/commands/health-check.md +1 -1
- package/pennyfarthing-dist/commands/orchestrator.md +1 -1
- package/pennyfarthing-dist/commands/parallel-work.md +2 -2
- package/pennyfarthing-dist/commands/pm.md +1 -1
- package/pennyfarthing-dist/commands/prime.md +18 -22
- package/pennyfarthing-dist/commands/reviewer.md +1 -1
- package/pennyfarthing-dist/commands/set-theme.md +1 -1
- package/pennyfarthing-dist/commands/sm.md +1 -1
- package/pennyfarthing-dist/commands/sprint.md +13 -4
- package/pennyfarthing-dist/commands/tea.md +1 -1
- package/pennyfarthing-dist/commands/tech-writer.md +1 -1
- package/pennyfarthing-dist/commands/ux-designer.md +1 -1
- package/pennyfarthing-dist/commands/work.md +2 -2
- package/pennyfarthing-dist/guides/agent-behavior.md +15 -1
- package/pennyfarthing-dist/personas/themes/rome.yaml +11 -11
- package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_bidirectional_sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/output.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/cli.py +168 -0
- package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/cli.py +8 -1
- package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/cli.py +144 -84
- package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/create.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_cli_modules.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_common.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_jira_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_package_structure.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_prime.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_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_workflow_cli.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/test_workflow_check.py +341 -0
- package/pennyfarthing_scripts/workflow.py +104 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Tests for pf workflow check command.
|
|
2
|
+
|
|
3
|
+
Story: MSSCI-12657 - Implement pf workflow check command
|
|
4
|
+
Epic: epic-67 (Pennyfarthing Python CLI)
|
|
5
|
+
|
|
6
|
+
Acceptance Criteria:
|
|
7
|
+
- [AC1] pf workflow check returns workflow state
|
|
8
|
+
- [AC2] --json flag outputs JSON format
|
|
9
|
+
- [AC3] Exit code 0 for all states (including empty)
|
|
10
|
+
|
|
11
|
+
These tests verify the `pf workflow check` CLI command behavior.
|
|
12
|
+
Tests should fail until the implementation is complete.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
from unittest.mock import patch
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
|
|
24
|
+
from click.testing import CliRunner
|
|
25
|
+
|
|
26
|
+
from pennyfarthing_scripts.cli import cli
|
|
27
|
+
from pennyfarthing_scripts.workflow import get_workflow_state
|
|
28
|
+
|
|
29
|
+
# Mock path: since cli.py does `from pennyfarthing_scripts.workflow import get_workflow_state`
|
|
30
|
+
# inside the command function, we mock at the source module
|
|
31
|
+
MOCK_PATH = "pennyfarthing_scripts.workflow.get_workflow_state"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestWorkflowCheckCLI:
|
|
35
|
+
"""Tests for the CLI entry point (AC1, AC2, AC3)."""
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
def runner(self) -> CliRunner:
|
|
39
|
+
"""Create a CLI test runner."""
|
|
40
|
+
return CliRunner()
|
|
41
|
+
|
|
42
|
+
def test_workflow_check_command_exists(self, runner: CliRunner) -> None:
|
|
43
|
+
"""AC1: workflow check command should exist and be invokable."""
|
|
44
|
+
result = runner.invoke(cli, ["workflow", "check", "--help"])
|
|
45
|
+
assert result.exit_code == 0
|
|
46
|
+
assert "workflow state" in result.output.lower()
|
|
47
|
+
|
|
48
|
+
def test_workflow_check_returns_state_field(self, runner: CliRunner) -> None:
|
|
49
|
+
"""AC1: workflow check should return state in output."""
|
|
50
|
+
with patch(MOCK_PATH) as mock_state:
|
|
51
|
+
mock_state.return_value = {"state": "IN_PROGRESS_STATE"}
|
|
52
|
+
result = runner.invoke(cli, ["workflow", "check"])
|
|
53
|
+
assert result.exit_code == 0
|
|
54
|
+
assert "State:" in result.output or "state" in result.output.lower()
|
|
55
|
+
|
|
56
|
+
def test_workflow_check_shows_story_id_when_present(
|
|
57
|
+
self, runner: CliRunner
|
|
58
|
+
) -> None:
|
|
59
|
+
"""AC1: workflow check should show story_id when in progress."""
|
|
60
|
+
with patch(MOCK_PATH) as mock_state:
|
|
61
|
+
mock_state.return_value = {
|
|
62
|
+
"state": "IN_PROGRESS_STATE",
|
|
63
|
+
"story_id": "MSSCI-12657",
|
|
64
|
+
"workflow": "tdd",
|
|
65
|
+
"phase": "red",
|
|
66
|
+
}
|
|
67
|
+
result = runner.invoke(cli, ["workflow", "check"])
|
|
68
|
+
assert result.exit_code == 0
|
|
69
|
+
assert "MSSCI-12657" in result.output
|
|
70
|
+
|
|
71
|
+
def test_workflow_check_shows_workflow_type(self, runner: CliRunner) -> None:
|
|
72
|
+
"""AC1: workflow check should show workflow type when in progress."""
|
|
73
|
+
with patch(MOCK_PATH) as mock_state:
|
|
74
|
+
mock_state.return_value = {
|
|
75
|
+
"state": "IN_PROGRESS_STATE",
|
|
76
|
+
"story_id": "MSSCI-12657",
|
|
77
|
+
"workflow": "tdd",
|
|
78
|
+
"phase": "red",
|
|
79
|
+
}
|
|
80
|
+
result = runner.invoke(cli, ["workflow", "check"])
|
|
81
|
+
assert result.exit_code == 0
|
|
82
|
+
assert "tdd" in result.output.lower()
|
|
83
|
+
|
|
84
|
+
def test_workflow_check_shows_phase(self, runner: CliRunner) -> None:
|
|
85
|
+
"""AC1: workflow check should show phase when in progress."""
|
|
86
|
+
with patch(MOCK_PATH) as mock_state:
|
|
87
|
+
mock_state.return_value = {
|
|
88
|
+
"state": "IN_PROGRESS_STATE",
|
|
89
|
+
"story_id": "MSSCI-12657",
|
|
90
|
+
"workflow": "tdd",
|
|
91
|
+
"phase": "red",
|
|
92
|
+
}
|
|
93
|
+
result = runner.invoke(cli, ["workflow", "check"])
|
|
94
|
+
assert result.exit_code == 0
|
|
95
|
+
assert "red" in result.output.lower()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TestWorkflowCheckJSONOutput:
|
|
99
|
+
"""Tests for JSON output format (AC2)."""
|
|
100
|
+
|
|
101
|
+
@pytest.fixture
|
|
102
|
+
def runner(self) -> CliRunner:
|
|
103
|
+
"""Create a CLI test runner."""
|
|
104
|
+
return CliRunner()
|
|
105
|
+
|
|
106
|
+
def test_json_flag_produces_valid_json(self, runner: CliRunner) -> None:
|
|
107
|
+
"""AC2: --json flag should output valid JSON."""
|
|
108
|
+
with patch(MOCK_PATH) as mock_state:
|
|
109
|
+
mock_state.return_value = {"state": "EMPTY_BACKLOG_STATE"}
|
|
110
|
+
result = runner.invoke(cli, ["workflow", "check", "--json"])
|
|
111
|
+
assert result.exit_code == 0
|
|
112
|
+
# Should be valid JSON
|
|
113
|
+
parsed = json.loads(result.output)
|
|
114
|
+
assert isinstance(parsed, dict)
|
|
115
|
+
|
|
116
|
+
def test_json_output_contains_state_field(self, runner: CliRunner) -> None:
|
|
117
|
+
"""AC2: JSON output should contain state field."""
|
|
118
|
+
with patch(MOCK_PATH) as mock_state:
|
|
119
|
+
mock_state.return_value = {"state": "NEW_WORK_STATE"}
|
|
120
|
+
result = runner.invoke(cli, ["workflow", "check", "--json"])
|
|
121
|
+
parsed = json.loads(result.output)
|
|
122
|
+
assert "state" in parsed
|
|
123
|
+
assert parsed["state"] == "NEW_WORK_STATE"
|
|
124
|
+
|
|
125
|
+
def test_json_output_contains_all_fields_when_in_progress(
|
|
126
|
+
self, runner: CliRunner
|
|
127
|
+
) -> None:
|
|
128
|
+
"""AC2: JSON output should contain all fields for in-progress state."""
|
|
129
|
+
with patch(MOCK_PATH) as mock_state:
|
|
130
|
+
mock_state.return_value = {
|
|
131
|
+
"state": "IN_PROGRESS_STATE",
|
|
132
|
+
"story_id": "MSSCI-12657",
|
|
133
|
+
"workflow": "tdd",
|
|
134
|
+
"phase": "implement",
|
|
135
|
+
}
|
|
136
|
+
result = runner.invoke(cli, ["workflow", "check", "--json"])
|
|
137
|
+
parsed = json.loads(result.output)
|
|
138
|
+
assert parsed["state"] == "IN_PROGRESS_STATE"
|
|
139
|
+
assert parsed["story_id"] == "MSSCI-12657"
|
|
140
|
+
assert parsed["workflow"] == "tdd"
|
|
141
|
+
assert parsed["phase"] == "implement"
|
|
142
|
+
|
|
143
|
+
def test_json_output_is_properly_formatted(self, runner: CliRunner) -> None:
|
|
144
|
+
"""AC2: JSON output should be formatted with indentation."""
|
|
145
|
+
with patch(MOCK_PATH) as mock_state:
|
|
146
|
+
mock_state.return_value = {
|
|
147
|
+
"state": "IN_PROGRESS_STATE",
|
|
148
|
+
"story_id": "TEST-123",
|
|
149
|
+
}
|
|
150
|
+
result = runner.invoke(cli, ["workflow", "check", "--json"])
|
|
151
|
+
# Should have newlines (indented JSON, not compact)
|
|
152
|
+
assert "\n" in result.output
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class TestWorkflowCheckExitCodes:
|
|
156
|
+
"""Tests for exit codes (AC3)."""
|
|
157
|
+
|
|
158
|
+
@pytest.fixture
|
|
159
|
+
def runner(self) -> CliRunner:
|
|
160
|
+
"""Create a CLI test runner."""
|
|
161
|
+
return CliRunner()
|
|
162
|
+
|
|
163
|
+
def test_exit_code_zero_for_empty_backlog(self, runner: CliRunner) -> None:
|
|
164
|
+
"""AC3: Exit code 0 for EMPTY_BACKLOG_STATE."""
|
|
165
|
+
with patch(MOCK_PATH) as mock_state:
|
|
166
|
+
mock_state.return_value = {"state": "EMPTY_BACKLOG_STATE"}
|
|
167
|
+
result = runner.invoke(cli, ["workflow", "check"])
|
|
168
|
+
assert result.exit_code == 0
|
|
169
|
+
|
|
170
|
+
def test_exit_code_zero_for_new_work(self, runner: CliRunner) -> None:
|
|
171
|
+
"""AC3: Exit code 0 for NEW_WORK_STATE."""
|
|
172
|
+
with patch(MOCK_PATH) as mock_state:
|
|
173
|
+
mock_state.return_value = {"state": "NEW_WORK_STATE"}
|
|
174
|
+
result = runner.invoke(cli, ["workflow", "check"])
|
|
175
|
+
assert result.exit_code == 0
|
|
176
|
+
|
|
177
|
+
def test_exit_code_zero_for_in_progress(self, runner: CliRunner) -> None:
|
|
178
|
+
"""AC3: Exit code 0 for IN_PROGRESS_STATE."""
|
|
179
|
+
with patch(MOCK_PATH) as mock_state:
|
|
180
|
+
mock_state.return_value = {
|
|
181
|
+
"state": "IN_PROGRESS_STATE",
|
|
182
|
+
"story_id": "MSSCI-12657",
|
|
183
|
+
"workflow": "tdd",
|
|
184
|
+
"phase": "red",
|
|
185
|
+
}
|
|
186
|
+
result = runner.invoke(cli, ["workflow", "check"])
|
|
187
|
+
assert result.exit_code == 0
|
|
188
|
+
|
|
189
|
+
def test_exit_code_zero_for_finish_state(self, runner: CliRunner) -> None:
|
|
190
|
+
"""AC3: Exit code 0 for FINISH_STATE."""
|
|
191
|
+
with patch(MOCK_PATH) as mock_state:
|
|
192
|
+
mock_state.return_value = {
|
|
193
|
+
"state": "FINISH_STATE",
|
|
194
|
+
"story_id": "MSSCI-12657",
|
|
195
|
+
"workflow": "tdd",
|
|
196
|
+
"phase": "approved",
|
|
197
|
+
}
|
|
198
|
+
result = runner.invoke(cli, ["workflow", "check"])
|
|
199
|
+
assert result.exit_code == 0
|
|
200
|
+
|
|
201
|
+
def test_exit_code_zero_for_json_mode(self, runner: CliRunner) -> None:
|
|
202
|
+
"""AC3: Exit code 0 with --json flag."""
|
|
203
|
+
with patch(MOCK_PATH) as mock_state:
|
|
204
|
+
mock_state.return_value = {"state": "EMPTY_BACKLOG_STATE"}
|
|
205
|
+
result = runner.invoke(cli, ["workflow", "check", "--json"])
|
|
206
|
+
assert result.exit_code == 0
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class TestGetWorkflowState:
|
|
210
|
+
"""Tests for the underlying get_workflow_state function."""
|
|
211
|
+
|
|
212
|
+
def test_returns_dict(self) -> None:
|
|
213
|
+
"""get_workflow_state should return a dictionary."""
|
|
214
|
+
result = get_workflow_state()
|
|
215
|
+
assert isinstance(result, dict)
|
|
216
|
+
|
|
217
|
+
def test_always_has_state_field(self) -> None:
|
|
218
|
+
"""get_workflow_state should always return a state field."""
|
|
219
|
+
result = get_workflow_state()
|
|
220
|
+
assert "state" in result
|
|
221
|
+
|
|
222
|
+
def test_state_is_valid_value(self) -> None:
|
|
223
|
+
"""get_workflow_state should return a valid state value."""
|
|
224
|
+
result = get_workflow_state()
|
|
225
|
+
valid_states = {
|
|
226
|
+
"EMPTY_BACKLOG_STATE",
|
|
227
|
+
"NEW_WORK_STATE",
|
|
228
|
+
"IN_PROGRESS_STATE",
|
|
229
|
+
"FINISH_STATE",
|
|
230
|
+
}
|
|
231
|
+
assert result["state"] in valid_states
|
|
232
|
+
|
|
233
|
+
def test_empty_backlog_when_no_session_dir(self, tmp_path: Path) -> None:
|
|
234
|
+
"""get_workflow_state should return EMPTY_BACKLOG_STATE when no .session dir."""
|
|
235
|
+
import os
|
|
236
|
+
|
|
237
|
+
original_dir = os.getcwd()
|
|
238
|
+
try:
|
|
239
|
+
os.chdir(tmp_path)
|
|
240
|
+
result = get_workflow_state()
|
|
241
|
+
# Without .session directory, should indicate empty or new work
|
|
242
|
+
assert result["state"] in {"EMPTY_BACKLOG_STATE", "NEW_WORK_STATE"}
|
|
243
|
+
finally:
|
|
244
|
+
os.chdir(original_dir)
|
|
245
|
+
|
|
246
|
+
def test_new_work_when_empty_session_dir(self, tmp_path: Path) -> None:
|
|
247
|
+
"""get_workflow_state should return NEW_WORK_STATE when .session is empty."""
|
|
248
|
+
import os
|
|
249
|
+
|
|
250
|
+
session_dir = tmp_path / ".session"
|
|
251
|
+
session_dir.mkdir()
|
|
252
|
+
|
|
253
|
+
original_dir = os.getcwd()
|
|
254
|
+
try:
|
|
255
|
+
os.chdir(tmp_path)
|
|
256
|
+
result = get_workflow_state()
|
|
257
|
+
assert result["state"] == "NEW_WORK_STATE"
|
|
258
|
+
finally:
|
|
259
|
+
os.chdir(original_dir)
|
|
260
|
+
|
|
261
|
+
def test_in_progress_with_session_file(self, tmp_path: Path) -> None:
|
|
262
|
+
"""get_workflow_state should return IN_PROGRESS_STATE with active session."""
|
|
263
|
+
import os
|
|
264
|
+
|
|
265
|
+
session_dir = tmp_path / ".session"
|
|
266
|
+
session_dir.mkdir()
|
|
267
|
+
|
|
268
|
+
session_file = session_dir / "MSSCI-12657-session.md"
|
|
269
|
+
session_file.write_text(
|
|
270
|
+
"""# Story Session: MSSCI-12657
|
|
271
|
+
|
|
272
|
+
## Story Details
|
|
273
|
+
- **Jira:** MSSCI-12657
|
|
274
|
+
- **Workflow:** tdd
|
|
275
|
+
- **Phase:** red
|
|
276
|
+
"""
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
original_dir = os.getcwd()
|
|
280
|
+
try:
|
|
281
|
+
os.chdir(tmp_path)
|
|
282
|
+
result = get_workflow_state()
|
|
283
|
+
assert result["state"] == "IN_PROGRESS_STATE"
|
|
284
|
+
assert result.get("story_id") == "MSSCI-12657"
|
|
285
|
+
assert result.get("workflow") == "tdd"
|
|
286
|
+
assert result.get("phase") == "red"
|
|
287
|
+
finally:
|
|
288
|
+
os.chdir(original_dir)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class TestSubprocessExecution:
|
|
292
|
+
"""Tests verifying the CLI works as a subprocess (integration)."""
|
|
293
|
+
|
|
294
|
+
def test_module_runnable(self) -> None:
|
|
295
|
+
"""CLI should be runnable as python -m pennyfarthing_scripts.cli."""
|
|
296
|
+
result = subprocess.run(
|
|
297
|
+
[sys.executable, "-m", "pennyfarthing_scripts.cli", "workflow", "check"],
|
|
298
|
+
capture_output=True,
|
|
299
|
+
text=True,
|
|
300
|
+
timeout=30,
|
|
301
|
+
)
|
|
302
|
+
# Should not crash - exit 0 for any valid state
|
|
303
|
+
assert result.returncode == 0
|
|
304
|
+
|
|
305
|
+
def test_module_with_json_flag(self) -> None:
|
|
306
|
+
"""CLI should accept --json flag when run as subprocess."""
|
|
307
|
+
result = subprocess.run(
|
|
308
|
+
[
|
|
309
|
+
sys.executable,
|
|
310
|
+
"-m",
|
|
311
|
+
"pennyfarthing_scripts.cli",
|
|
312
|
+
"workflow",
|
|
313
|
+
"check",
|
|
314
|
+
"--json",
|
|
315
|
+
],
|
|
316
|
+
capture_output=True,
|
|
317
|
+
text=True,
|
|
318
|
+
timeout=30,
|
|
319
|
+
)
|
|
320
|
+
assert result.returncode == 0
|
|
321
|
+
# Output should be valid JSON
|
|
322
|
+
parsed = json.loads(result.stdout)
|
|
323
|
+
assert "state" in parsed
|
|
324
|
+
|
|
325
|
+
def test_help_flag(self) -> None:
|
|
326
|
+
"""CLI should show help with --help flag."""
|
|
327
|
+
result = subprocess.run(
|
|
328
|
+
[
|
|
329
|
+
sys.executable,
|
|
330
|
+
"-m",
|
|
331
|
+
"pennyfarthing_scripts.cli",
|
|
332
|
+
"workflow",
|
|
333
|
+
"check",
|
|
334
|
+
"--help",
|
|
335
|
+
],
|
|
336
|
+
capture_output=True,
|
|
337
|
+
text=True,
|
|
338
|
+
timeout=30,
|
|
339
|
+
)
|
|
340
|
+
assert result.returncode == 0
|
|
341
|
+
assert "workflow state" in result.stdout.lower()
|
|
@@ -181,3 +181,107 @@ def get_scale_level_info(level: int) -> dict[str, Any]:
|
|
|
181
181
|
return {"level": level, "scope": "unknown", "stories_min": 0,
|
|
182
182
|
"stories_max": 0, "workflow": "prd", "artifacts": []}
|
|
183
183
|
return SCALE_LEVELS[level].copy()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# Phase ownership mapping for TDD workflow
|
|
187
|
+
# Canonical YAML names: setup, red, green, review, finish
|
|
188
|
+
TDD_PHASE_OWNERS: dict[str, str] = {
|
|
189
|
+
"setup": "sm",
|
|
190
|
+
"red": "tea",
|
|
191
|
+
"green": "dev",
|
|
192
|
+
"review": "reviewer",
|
|
193
|
+
"finish": "sm",
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# Phase ownership mapping for trivial workflow (no TEA)
|
|
197
|
+
# Canonical YAML names: setup, impl, review, finish
|
|
198
|
+
TRIVIAL_PHASE_OWNERS: dict[str, str] = {
|
|
199
|
+
"setup": "sm",
|
|
200
|
+
"impl": "dev",
|
|
201
|
+
"review": "reviewer",
|
|
202
|
+
"finish": "sm",
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# All workflow phase mappings
|
|
206
|
+
WORKFLOW_PHASES: dict[str, dict[str, str]] = {
|
|
207
|
+
"tdd": TDD_PHASE_OWNERS,
|
|
208
|
+
"trivial": TRIVIAL_PHASE_OWNERS,
|
|
209
|
+
"bdd": TDD_PHASE_OWNERS, # BDD uses same phases as TDD
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def get_phase_owner(workflow: str, phase: str) -> str:
|
|
214
|
+
"""Get the agent that owns a workflow phase.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
workflow: Workflow name (tdd, trivial, bdd)
|
|
218
|
+
phase: Phase name (setup, red, implement, review, approved)
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Agent name (sm, tea, dev, reviewer)
|
|
222
|
+
"""
|
|
223
|
+
phases = WORKFLOW_PHASES.get(workflow, TDD_PHASE_OWNERS)
|
|
224
|
+
return phases.get(phase, "sm")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def get_workflow_state() -> dict[str, Any]:
|
|
228
|
+
"""Get current workflow state from session files.
|
|
229
|
+
|
|
230
|
+
Scans .session/ directory for active session files and extracts
|
|
231
|
+
workflow state information.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Dict with state, story_id, workflow, phase fields
|
|
235
|
+
"""
|
|
236
|
+
from pathlib import Path
|
|
237
|
+
|
|
238
|
+
# Look for session files in .session/
|
|
239
|
+
session_dir = Path(".session")
|
|
240
|
+
if not session_dir.exists():
|
|
241
|
+
return {"state": "EMPTY_BACKLOG_STATE"}
|
|
242
|
+
|
|
243
|
+
# Find session files (pattern: *-session.md)
|
|
244
|
+
session_files = list(session_dir.glob("*-session.md"))
|
|
245
|
+
|
|
246
|
+
# Filter out workflow session files and archived files
|
|
247
|
+
story_sessions = [
|
|
248
|
+
f for f in session_files
|
|
249
|
+
if not f.name.startswith("prd-")
|
|
250
|
+
and not f.name.startswith("architecture-")
|
|
251
|
+
and not f.name.startswith("research-")
|
|
252
|
+
and "workflow" not in f.name.lower()
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
if not story_sessions:
|
|
256
|
+
return {"state": "NEW_WORK_STATE"}
|
|
257
|
+
|
|
258
|
+
# Read the most recent session file
|
|
259
|
+
session_file = max(story_sessions, key=lambda f: f.stat().st_mtime)
|
|
260
|
+
content = session_file.read_text()
|
|
261
|
+
|
|
262
|
+
# Extract fields from markdown format
|
|
263
|
+
# Session files use list format: "- **Field:** value"
|
|
264
|
+
# Also handle direct format: "**Field:** value"
|
|
265
|
+
result: dict[str, Any] = {"state": "IN_PROGRESS_STATE"}
|
|
266
|
+
|
|
267
|
+
for line in content.split("\n"):
|
|
268
|
+
# Strip leading "- " for list items
|
|
269
|
+
stripped = line.lstrip("- ").strip()
|
|
270
|
+
|
|
271
|
+
if stripped.startswith("**Story:**"):
|
|
272
|
+
result["story_id"] = stripped.replace("**Story:**", "").strip()
|
|
273
|
+
elif stripped.startswith("**Jira:**"):
|
|
274
|
+
result["story_id"] = stripped.replace("**Jira:**", "").strip()
|
|
275
|
+
elif stripped.startswith("**ID:**"):
|
|
276
|
+
# Also check **ID:** field (used in Story Details section)
|
|
277
|
+
if "story_id" not in result:
|
|
278
|
+
result["story_id"] = stripped.replace("**ID:**", "").strip()
|
|
279
|
+
elif stripped.startswith("**Type:**"):
|
|
280
|
+
# Workflow section uses **Type:** not **Workflow:**
|
|
281
|
+
result["workflow"] = stripped.replace("**Type:**", "").strip()
|
|
282
|
+
elif stripped.startswith("**Workflow:**"):
|
|
283
|
+
result["workflow"] = stripped.replace("**Workflow:**", "").strip()
|
|
284
|
+
elif stripped.startswith("**Phase:**"):
|
|
285
|
+
result["phase"] = stripped.replace("**Phase:**", "").strip()
|
|
286
|
+
|
|
287
|
+
return result
|