@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.
Files changed (63) hide show
  1. package/package.json +1 -1
  2. package/pennyfarthing-dist/agents/reviewer.md +21 -4
  3. package/pennyfarthing-dist/agents/sm.md +10 -0
  4. package/pennyfarthing-dist/commands/architect.md +1 -1
  5. package/pennyfarthing-dist/commands/dev.md +1 -1
  6. package/pennyfarthing-dist/commands/devops.md +1 -1
  7. package/pennyfarthing-dist/commands/health-check.md +1 -1
  8. package/pennyfarthing-dist/commands/orchestrator.md +1 -1
  9. package/pennyfarthing-dist/commands/parallel-work.md +2 -2
  10. package/pennyfarthing-dist/commands/pm.md +1 -1
  11. package/pennyfarthing-dist/commands/prime.md +18 -22
  12. package/pennyfarthing-dist/commands/reviewer.md +1 -1
  13. package/pennyfarthing-dist/commands/set-theme.md +1 -1
  14. package/pennyfarthing-dist/commands/sm.md +1 -1
  15. package/pennyfarthing-dist/commands/sprint.md +13 -4
  16. package/pennyfarthing-dist/commands/tea.md +1 -1
  17. package/pennyfarthing-dist/commands/tech-writer.md +1 -1
  18. package/pennyfarthing-dist/commands/ux-designer.md +1 -1
  19. package/pennyfarthing-dist/commands/work.md +2 -2
  20. package/pennyfarthing-dist/guides/agent-behavior.md +15 -1
  21. package/pennyfarthing-dist/personas/themes/rome.yaml +11 -11
  22. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  23. package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
  24. package/pennyfarthing_scripts/__pycache__/jira_bidirectional_sync.cpython-314.pyc +0 -0
  25. package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
  26. package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
  27. package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
  28. package/pennyfarthing_scripts/__pycache__/output.cpython-314.pyc +0 -0
  29. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  30. package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
  31. package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
  32. package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
  33. package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
  34. package/pennyfarthing_scripts/cli.py +168 -0
  35. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  36. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  37. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  38. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  39. package/pennyfarthing_scripts/prime/cli.py +8 -1
  40. package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
  41. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  42. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  43. package/pennyfarthing_scripts/sprint/cli.py +144 -84
  44. package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
  45. package/pennyfarthing_scripts/story/__pycache__/__main__.cpython-314.pyc +0 -0
  46. package/pennyfarthing_scripts/story/__pycache__/cli.cpython-314.pyc +0 -0
  47. package/pennyfarthing_scripts/story/__pycache__/create.cpython-314.pyc +0 -0
  48. package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
  49. package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
  50. package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
  51. package/pennyfarthing_scripts/tests/__pycache__/test_cli_modules.cpython-314-pytest-9.0.2.pyc +0 -0
  52. package/pennyfarthing_scripts/tests/__pycache__/test_common.cpython-314-pytest-9.0.2.pyc +0 -0
  53. package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  54. package/pennyfarthing_scripts/tests/__pycache__/test_jira_package.cpython-314-pytest-9.0.2.pyc +0 -0
  55. package/pennyfarthing_scripts/tests/__pycache__/test_package_structure.cpython-314-pytest-9.0.2.pyc +0 -0
  56. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  57. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
  58. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  59. package/pennyfarthing_scripts/tests/__pycache__/test_story_package.cpython-314-pytest-9.0.2.pyc +0 -0
  60. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
  61. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_cli.cpython-314-pytest-9.0.2.pyc +0 -0
  62. package/pennyfarthing_scripts/tests/test_workflow_check.py +341 -0
  63. 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