@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.
Files changed (168) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/packages/core/dist/cli/commands/doctor-legacy.test.js +2 -2
  4. package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -1
  5. package/packages/core/dist/cli/commands/doctor.d.ts +63 -0
  6. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  7. package/packages/core/dist/cli/commands/doctor.js +280 -43
  8. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  9. package/packages/core/dist/cli/commands/init.d.ts +12 -0
  10. package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
  11. package/packages/core/dist/cli/commands/init.js +45 -0
  12. package/packages/core/dist/cli/commands/init.js.map +1 -1
  13. package/packages/core/dist/cli/commands/pyproject-install.test.d.ts +19 -0
  14. package/packages/core/dist/cli/commands/pyproject-install.test.d.ts.map +1 -0
  15. package/packages/core/dist/cli/commands/pyproject-install.test.js +261 -0
  16. package/packages/core/dist/cli/commands/pyproject-install.test.js.map +1 -0
  17. package/packages/core/dist/cli/commands/update-consolidation.test.js +14 -6
  18. package/packages/core/dist/cli/commands/update-consolidation.test.js.map +1 -1
  19. package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
  20. package/packages/core/dist/cli/commands/update.js +5 -1
  21. package/packages/core/dist/cli/commands/update.js.map +1 -1
  22. package/packages/core/dist/cli/index.js +2 -0
  23. package/packages/core/dist/cli/index.js.map +1 -1
  24. package/packages/core/dist/cli/utils/python.d.ts +1 -0
  25. package/packages/core/dist/cli/utils/python.d.ts.map +1 -1
  26. package/packages/core/dist/cli/utils/python.js +22 -1
  27. package/packages/core/dist/cli/utils/python.js.map +1 -1
  28. package/packages/core/dist/cli/utils/settings-hook-migration.test.d.ts +17 -0
  29. package/packages/core/dist/cli/utils/settings-hook-migration.test.d.ts.map +1 -0
  30. package/packages/core/dist/cli/utils/settings-hook-migration.test.js +382 -0
  31. package/packages/core/dist/cli/utils/settings-hook-migration.test.js.map +1 -0
  32. package/packages/core/dist/cli/utils/settings-pf-wrapper.test.d.ts +16 -0
  33. package/packages/core/dist/cli/utils/settings-pf-wrapper.test.d.ts.map +1 -0
  34. package/packages/core/dist/cli/utils/settings-pf-wrapper.test.js +377 -0
  35. package/packages/core/dist/cli/utils/settings-pf-wrapper.test.js.map +1 -0
  36. package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
  37. package/packages/core/dist/cli/utils/settings.js +15 -2
  38. package/packages/core/dist/cli/utils/settings.js.map +1 -1
  39. package/packages/core/dist/server/paths.d.ts.map +1 -1
  40. package/packages/core/dist/server/paths.js +6 -0
  41. package/packages/core/dist/server/paths.js.map +1 -1
  42. package/packages/core/dist/server/settings.d.ts.map +1 -1
  43. package/packages/core/dist/server/settings.js +5 -0
  44. package/packages/core/dist/server/settings.js.map +1 -1
  45. package/packages/core/dist/workflow/tandem-workflow-templates.test.js +7 -5
  46. package/packages/core/dist/workflow/tandem-workflow-templates.test.js.map +1 -1
  47. package/packages/core/dist/workflow/workflow-graph-validation.d.ts +65 -0
  48. package/packages/core/dist/workflow/workflow-graph-validation.d.ts.map +1 -0
  49. package/packages/core/dist/workflow/workflow-graph-validation.js +190 -0
  50. package/packages/core/dist/workflow/workflow-graph-validation.js.map +1 -0
  51. package/packages/core/dist/workflow/workflow-graph-validation.test.d.ts +18 -0
  52. package/packages/core/dist/workflow/workflow-graph-validation.test.d.ts.map +1 -0
  53. package/packages/core/dist/workflow/workflow-graph-validation.test.js +706 -0
  54. package/packages/core/dist/workflow/workflow-graph-validation.test.js.map +1 -0
  55. package/packages/core/dist/workflow/workflow-migration.test.js +6 -5
  56. package/packages/core/dist/workflow/workflow-migration.test.js.map +1 -1
  57. package/pennyfarthing-dist/agents/dev.md +4 -2
  58. package/pennyfarthing-dist/agents/devops.md +2 -10
  59. package/pennyfarthing-dist/agents/reviewer-preflight.md +4 -5
  60. package/pennyfarthing-dist/agents/sm.md +4 -17
  61. package/pennyfarthing-dist/commands/pf-health-check.md +30 -11
  62. package/pennyfarthing-dist/gates/{confidence-sm.md → confidence.md} +16 -17
  63. package/pennyfarthing-dist/gates/dev-exit.md +75 -0
  64. package/pennyfarthing-dist/gates/merge-ready.md +49 -0
  65. package/pennyfarthing-dist/gates/release-ready.md +95 -0
  66. package/pennyfarthing-dist/gates/reviewer-preflight-check.md +90 -0
  67. package/pennyfarthing-dist/gates/sm-setup-exit.md +82 -0
  68. package/pennyfarthing-dist/guides/agent-behavior.md +88 -30
  69. package/pennyfarthing-dist/guides/gates.md +7 -2
  70. package/pennyfarthing-dist/scripts/lib/find-root.sh +5 -0
  71. package/pennyfarthing-dist/scripts/lib/run-pf.sh +7 -0
  72. package/pennyfarthing-dist/skills/pf-settings/skill.md +42 -0
  73. package/pennyfarthing-dist/skills/skill-registry.yaml +15 -0
  74. package/pennyfarthing-dist/templates/pyproject.toml +27 -0
  75. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +7 -3
  76. package/pennyfarthing-dist/workflows/bdd.yaml +7 -3
  77. package/pennyfarthing-dist/workflows/installation-check/steps/step-01-foundation.md +77 -0
  78. package/pennyfarthing-dist/workflows/installation-check/steps/step-02-commands.md +82 -0
  79. package/pennyfarthing-dist/workflows/installation-check/steps/step-03-hooks.md +121 -0
  80. package/pennyfarthing-dist/workflows/installation-check/steps/step-04-scripts.md +83 -0
  81. package/pennyfarthing-dist/workflows/installation-check/steps/step-05-layout.md +81 -0
  82. package/pennyfarthing-dist/workflows/installation-check/steps/step-06-legacy.md +94 -0
  83. package/pennyfarthing-dist/workflows/installation-check/steps/step-07-tools.md +80 -0
  84. package/pennyfarthing-dist/workflows/installation-check/steps/step-08-summary.md +99 -0
  85. package/pennyfarthing-dist/workflows/installation-check/workflow.yaml +47 -0
  86. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +7 -3
  87. package/pennyfarthing-dist/workflows/tdd.yaml +7 -3
  88. package/pennyfarthing-dist/workflows/trivial.yaml +7 -3
  89. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  90. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  91. package/pennyfarthing_scripts/__pycache__/context.cpython-314.pyc +0 -0
  92. package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
  93. package/pennyfarthing_scripts/bc/__pycache__/focus.cpython-314.pyc +0 -0
  94. package/pennyfarthing_scripts/bc/__pycache__/split.cpython-314.pyc +0 -0
  95. package/pennyfarthing_scripts/bc/cli.py +21 -0
  96. package/pennyfarthing_scripts/bc/focus.py +1 -0
  97. package/pennyfarthing_scripts/bc/split.py +52 -0
  98. package/pennyfarthing_scripts/bikerack/__pycache__/audit_log_panel.cpython-314.pyc +0 -0
  99. package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
  100. package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
  101. package/pennyfarthing_scripts/bikerack/__pycache__/context_meter_footer.cpython-314.pyc +0 -0
  102. package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
  103. package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
  104. package/pennyfarthing_scripts/bikerack/__pycache__/events.cpython-314.pyc +0 -0
  105. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  106. package/pennyfarthing_scripts/bikerack/__pycache__/portrait_resolver.cpython-314.pyc +0 -0
  107. package/pennyfarthing_scripts/bikerack/__pycache__/progress_panel.cpython-314.pyc +0 -0
  108. package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
  109. package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
  110. package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
  111. package/pennyfarthing_scripts/bikerack/context_meter_footer.py +53 -3
  112. package/pennyfarthing_scripts/bikerack/tui.py +202 -8
  113. package/pennyfarthing_scripts/bmad/__init__.py +1 -0
  114. package/pennyfarthing_scripts/bmad/__pycache__/__init__.cpython-314.pyc +0 -0
  115. package/pennyfarthing_scripts/bmad/__pycache__/cli.cpython-314.pyc +0 -0
  116. package/pennyfarthing_scripts/bmad/__pycache__/parser.cpython-314.pyc +0 -0
  117. package/pennyfarthing_scripts/bmad/__pycache__/sync.cpython-314.pyc +0 -0
  118. package/pennyfarthing_scripts/bmad/__pycache__/test_parser.cpython-314-pytest-9.0.2.pyc +0 -0
  119. package/pennyfarthing_scripts/bmad/__pycache__/test_sync.cpython-314-pytest-9.0.2.pyc +0 -0
  120. package/pennyfarthing_scripts/bmad/cli.py +197 -0
  121. package/pennyfarthing_scripts/bmad/importer.py +200 -0
  122. package/pennyfarthing_scripts/bmad/parser.py +233 -0
  123. package/pennyfarthing_scripts/bmad/sync.py +464 -0
  124. package/pennyfarthing_scripts/bmad/test_parser.py +253 -0
  125. package/pennyfarthing_scripts/bmad/test_sync.py +223 -0
  126. package/pennyfarthing_scripts/cli.py +10 -0
  127. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  128. package/pennyfarthing_scripts/consultation/__pycache__/cli.cpython-314.pyc +0 -0
  129. package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
  130. package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
  131. package/pennyfarthing_scripts/handoff/__pycache__/gate_file.cpython-314.pyc +0 -0
  132. package/pennyfarthing_scripts/handoff/__pycache__/gate_runner.cpython-314.pyc +0 -0
  133. package/pennyfarthing_scripts/handoff/__pycache__/marker.cpython-314.pyc +0 -0
  134. package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
  135. package/pennyfarthing_scripts/hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  136. package/pennyfarthing_scripts/hooks/__pycache__/bell_mode.cpython-314.pyc +0 -0
  137. package/pennyfarthing_scripts/hooks/__pycache__/cli.cpython-314.pyc +0 -0
  138. package/pennyfarthing_scripts/hooks/__pycache__/context_breaker.cpython-314.pyc +0 -0
  139. package/pennyfarthing_scripts/hooks/__pycache__/context_warning.cpython-314.pyc +0 -0
  140. package/pennyfarthing_scripts/hooks/__pycache__/cyclist_pretooluse.cpython-314.pyc +0 -0
  141. package/pennyfarthing_scripts/hooks/__pycache__/pre_edit_check.cpython-314.pyc +0 -0
  142. package/pennyfarthing_scripts/hooks/__pycache__/reflector_check.cpython-314.pyc +0 -0
  143. package/pennyfarthing_scripts/hooks/__pycache__/schema_validation.cpython-314.pyc +0 -0
  144. package/pennyfarthing_scripts/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
  145. package/pennyfarthing_scripts/hooks/__pycache__/session_stop.cpython-314.pyc +0 -0
  146. package/pennyfarthing_scripts/hooks/__pycache__/sprint_yaml_validation.cpython-314.pyc +0 -0
  147. package/pennyfarthing_scripts/hooks/__pycache__/statusline.cpython-314.pyc +0 -0
  148. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  149. package/pennyfarthing_scripts/settings/__init__.py +0 -0
  150. package/pennyfarthing_scripts/settings/__pycache__/__init__.cpython-314.pyc +0 -0
  151. package/pennyfarthing_scripts/settings/__pycache__/cli.cpython-314.pyc +0 -0
  152. package/pennyfarthing_scripts/settings/__pycache__/settings.cpython-314.pyc +0 -0
  153. package/pennyfarthing_scripts/settings/cli.py +55 -0
  154. package/pennyfarthing_scripts/settings/settings.py +98 -0
  155. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  156. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  157. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  158. package/pennyfarthing_scripts/tests/__pycache__/test_confidence_sm_evaluation.cpython-314-pytest-9.0.2.pyc +0 -0
  159. package/pennyfarthing_scripts/tests/__pycache__/test_confidence_sm_gate.cpython-314-pytest-9.0.2.pyc +0 -0
  160. package/pennyfarthing_scripts/tests/__pycache__/test_gate_file_resolution.cpython-314-pytest-9.0.2.pyc +0 -0
  161. package/pennyfarthing_scripts/tests/__pycache__/test_gate_runner.cpython-314-pytest-9.0.2.pyc +0 -0
  162. package/pennyfarthing_scripts/tests/__pycache__/test_resolve_gate_file_field.cpython-314-pytest-9.0.2.pyc +0 -0
  163. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_list_team.cpython-314.pyc +0 -0
  164. package/pennyfarthing_scripts/tests/test_confidence_sm_gate.py +17 -16
  165. package/pennyfarthing_scripts/tests/test_resolve_gate_file_field.py +45 -47
  166. package/pennyfarthing_scripts/tests/test_workflow_list_team.py +0 -4
  167. package/pennyfarthing_scripts/workflow/__pycache__/cli.cpython-314.pyc +0 -0
  168. package/pennyfarthing_scripts/workflow/__pycache__/state.cpython-314.pyc +0 -0
@@ -0,0 +1,98 @@
1
+ """
2
+ Settings logic for reading and writing .pennyfarthing/config.local.yaml.
3
+
4
+ Provides dot-path traversal for get/set operations with type coercion.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ import yaml
10
+
11
+ from pennyfarthing_scripts.common.config import get_project_root, load_pennyfarthing_config
12
+
13
+ # Top-level keys to show in `pf settings show` (skip layout/panel blobs)
14
+ SHOW_KEYS = ("theme", "workflow", "display", "split", "last_panel")
15
+
16
+
17
+ def _coerce_value(value: str) -> Any:
18
+ """Coerce a string value to bool, int, or leave as str."""
19
+ lower = value.lower()
20
+ if lower == "true":
21
+ return True
22
+ if lower == "false":
23
+ return False
24
+ try:
25
+ return int(value)
26
+ except ValueError:
27
+ return value
28
+
29
+
30
+ def _get_by_path(data: dict, key: str) -> Any:
31
+ """Traverse a dict by dot-separated path.
32
+
33
+ Returns the value at the path, or raises KeyError if not found.
34
+ """
35
+ parts = key.split(".")
36
+ current: Any = data
37
+ for part in parts:
38
+ if not isinstance(current, dict) or part not in current:
39
+ raise KeyError(key)
40
+ current = current[part]
41
+ return current
42
+
43
+
44
+ def _set_by_path(data: dict, key: str, value: Any) -> None:
45
+ """Set a value in a dict by dot-separated path, creating intermediates."""
46
+ parts = key.split(".")
47
+ current = data
48
+ for part in parts[:-1]:
49
+ if part not in current or not isinstance(current[part], dict):
50
+ current[part] = {}
51
+ current = current[part]
52
+ current[parts[-1]] = value
53
+
54
+
55
+ def get_setting(key: str) -> Any:
56
+ """Get a setting value by dot-path."""
57
+ config = load_pennyfarthing_config()
58
+ return _get_by_path(config, key)
59
+
60
+
61
+ def set_setting(key: str, value: str) -> dict:
62
+ """Set a setting value by dot-path. Returns the updated config."""
63
+ root = get_project_root()
64
+ config_path = root / ".pennyfarthing" / "config.local.yaml"
65
+
66
+ config = load_pennyfarthing_config(root)
67
+ coerced = _coerce_value(value)
68
+ _set_by_path(config, key, coerced)
69
+
70
+ config_path.parent.mkdir(parents=True, exist_ok=True)
71
+ with open(config_path, "w") as f:
72
+ yaml.dump(config, f, default_flow_style=False, sort_keys=False)
73
+
74
+ return config
75
+
76
+
77
+ def show_settings() -> str:
78
+ """Format interesting top-level settings for display."""
79
+ config = load_pennyfarthing_config()
80
+ lines: list[str] = []
81
+
82
+ for key in SHOW_KEYS:
83
+ if key not in config:
84
+ continue
85
+ val = config[key]
86
+ if isinstance(val, dict):
87
+ lines.append(f"{key}:")
88
+ for k, v in val.items():
89
+ if isinstance(v, dict):
90
+ nested = yaml.dump({k: v}, default_flow_style=False, sort_keys=False)
91
+ for line in nested.rstrip().split("\n"):
92
+ lines.append(f" {line}")
93
+ else:
94
+ lines.append(f" {k}: {v}")
95
+ else:
96
+ lines.append(f"{key}: {val}")
97
+
98
+ return "\n".join(lines) if lines else "(no settings found)"
@@ -1,16 +1,18 @@
1
- """Tests for SM confidence gate file — Story 90-2.
1
+ """Tests for confidence gate file — Story 90-2 (generalized).
2
2
 
3
3
  Epic: 90 (Confidence Circuit Breaker via Gate)
4
- Story: 90-2 — Implement SM confidence gate file
4
+ Story: 90-2 — Implement confidence gate file
5
5
 
6
- Tests the confidence-sm gate file that checks whether an instruction to the
7
- SM agent is ambiguous. If ambiguous, <fail> returns clarifying options. If
6
+ Tests the confidence gate file that checks whether an instruction to any
7
+ agent is ambiguous. If ambiguous, <fail> returns clarifying options. If
8
8
  unambiguous, <pass> lets the agent proceed.
9
9
 
10
+ Originally SM-specific (confidence-sm), generalized to agent-agnostic (confidence).
11
+
10
12
  Acceptance Criteria:
11
13
  - [AC1] Gate file exists in pennyfarthing-dist/gates/ following Gate PRD schema
12
14
  - [AC2] Gate has <gate>, <purpose>, <pass>, <fail> blocks
13
- - [AC3] Gate checks whether SM instruction is ambiguous
15
+ - [AC3] Gate checks whether instruction is ambiguous
14
16
  - [AC4] <fail> block returns clarifying options when ambiguous
15
17
  - [AC5] <pass> block lets the agent proceed when unambiguous
16
18
  - [AC6] Gate uses model="haiku"
@@ -30,7 +32,7 @@ from pennyfarthing_scripts.handoff.gate_runner import parse_gate_file
30
32
  # Fixtures
31
33
  # ---------------------------------------------------------------------------
32
34
 
33
- GATE_NAME = "confidence-sm"
35
+ GATE_NAME = "confidence"
34
36
 
35
37
  # The gate file lives in pennyfarthing-dist/gates/ relative to the framework root
36
38
  # In the dogfooding context, the project root is the orchestrator, so we need
@@ -43,7 +45,7 @@ _GATE_FILE = _FRAMEWORK_ROOT / "pennyfarthing-dist" / "gates" / f"{GATE_NAME}.md
43
45
 
44
46
  @pytest.fixture
45
47
  def gate_path() -> Path:
46
- """Return the expected path to the confidence-sm gate file."""
48
+ """Return the expected path to the confidence gate file."""
47
49
  return _GATE_FILE
48
50
 
49
51
 
@@ -69,7 +71,7 @@ class TestGateFileExists:
69
71
  """AC1: Gate file exists at the expected location."""
70
72
 
71
73
  def test_gate_file_exists(self, gate_path: Path) -> None:
72
- """AC1: confidence-sm.md exists in pennyfarthing-dist/gates/."""
74
+ """AC1: confidence.md exists in pennyfarthing-dist/gates/."""
73
75
  assert gate_path.is_file(), f"Gate file not found: {gate_path}"
74
76
 
75
77
  def test_gate_file_not_empty(self, gate_path: Path) -> None:
@@ -138,11 +140,10 @@ class TestGateSchemaStructure:
138
140
 
139
141
 
140
142
  class TestGateAmbiguityDetection:
141
- """AC3: Gate content describes checking for ambiguous SM instructions."""
143
+ """AC3: Gate content describes checking for ambiguous instructions."""
142
144
 
143
145
  def test_purpose_mentions_ambiguity(self, gate_content: str) -> None:
144
146
  """AC3: Purpose section references ambiguity or unclear instructions."""
145
- # Extract purpose content between tags
146
147
  import re
147
148
 
148
149
  purpose_match = re.search(
@@ -155,17 +156,17 @@ class TestGateAmbiguityDetection:
155
156
  for term in ["ambig", "unclear", "vague", "confidence", "clarif"]
156
157
  ), f"Purpose doesn't reference ambiguity: {purpose}"
157
158
 
158
- def test_gate_name_is_confidence_sm(self, parsed_gate: dict) -> None:
159
- """AC3: Gate name is 'confidence-sm'."""
159
+ def test_gate_name_is_confidence(self, parsed_gate: dict) -> None:
160
+ """AC3: Gate name is 'confidence'."""
160
161
  assert parsed_gate["name"] == GATE_NAME
161
162
 
162
- def test_content_references_sm_agent(self, gate_content: str) -> None:
163
- """AC3: Gate content references the SM agent or scrum master role."""
163
+ def test_content_is_agent_agnostic(self, gate_content: str) -> None:
164
+ """AC3: Gate content is agent-agnostic (not SM-specific)."""
164
165
  content_lower = gate_content.lower()
165
166
  assert any(
166
167
  term in content_lower
167
- for term in ["sm agent", "scrum master", "sm ", "story management"]
168
- ), "Gate doesn't reference SM agent"
168
+ for term in ["current agent", "the agent", "any agent"]
169
+ ), "Gate should be agent-agnostic"
169
170
 
170
171
 
171
172
  # ===========================================================================
@@ -6,7 +6,7 @@ Story: 106-3 — Workflow YAML gate.file integration
6
6
  Tests the gate.file field support in resolve_gate():
7
7
  - Schema extension: gate.file extracted from workflow YAML
8
8
  - Backward compatibility: gate.type-only workflows unchanged
9
- - TDD workflow migration: green phase has file: gates/tests-pass
9
+ - TDD workflow migration: all phases now have gate.file fields
10
10
 
11
11
  Acceptance Criteria:
12
12
  - [AC1] resolve-gate.py reads gate.file field from workflow YAML phases
@@ -15,9 +15,9 @@ Acceptance Criteria:
15
15
  - [AC2] Workflows with only gate.type continue to work unchanged
16
16
  - [AC2] Existing gate type logic remains functional
17
17
  - [AC2] No breaking changes to resolve-gate API
18
- - [AC3] Green phase in tdd.yaml has file: gates/tests-pass and type: tests_pass
19
- - [AC3] Other phases remain unchanged (backward compat period)
20
- - [AC3] File path is relative: gates/tests-pass (not absolute)
18
+ - [AC3] Green phase in tdd.yaml has file: gates/dev-exit and type: dev_exit
19
+ - [AC3] All phases have gate.file fields (full migration complete)
20
+ - [AC3] File paths are relative (not absolute)
21
21
  """
22
22
 
23
23
  from __future__ import annotations
@@ -202,21 +202,19 @@ class TestResolveGateFileOnly:
202
202
  )
203
203
  assert result["gate_type"] is None
204
204
 
205
- def test_status_skip_when_no_type_and_file_only(
205
+ def test_status_ready_when_no_type_and_file_only(
206
206
  self, project: Path
207
207
  ) -> None:
208
- """AC1: gate with file-only and no type → status depends on gate_type logic.
208
+ """AC1: gate with file-only and no type → status is 'ready'.
209
209
 
210
- Current resolve_gate checks gate_type for skip logic:
211
- - gate_type == 'manual' → skip
212
- - gate_type is None → skip
213
- So file-only gates (no type) currently get 'skip' status.
214
- This is expected during migration — consumers check gate_file separately.
210
+ File-only gates (no type) are resolvable — the gate runner uses
211
+ gate_file to locate and execute the gate. Status is 'ready' when
212
+ an assessment exists.
215
213
  """
216
214
  result = resolve_gate(
217
215
  "106-3", "file-only", "green", project_root=project
218
216
  )
219
- assert result["status"] == "skip"
217
+ assert result["status"] == "ready"
220
218
 
221
219
 
222
220
  # ===========================================================================
@@ -315,11 +313,10 @@ class TestResolveGateBackwardCompat:
315
313
 
316
314
 
317
315
  class TestTddWorkflowMigration:
318
- """AC3: TDD workflow green phase has file: gates/tests-pass.
316
+ """AC3: TDD workflow phases have gate.file fields after full migration.
319
317
 
320
318
  These tests read the ACTUAL tdd.yaml from the project to verify
321
- the migration has been applied. They will FAIL until the Dev
322
- updates tdd.yaml.
319
+ the migration has been applied.
323
320
  """
324
321
 
325
322
  @pytest.fixture
@@ -342,54 +339,55 @@ class TestTddWorkflowMigration:
342
339
  raise ValueError(f"Phase '{name}' not found")
343
340
 
344
341
  def test_green_phase_has_gate_file(self, tdd_yaml: dict) -> None:
345
- """AC3: Green phase should have gate.file = 'gates/tests-pass'."""
342
+ """AC3: Green phase should have gate.file = 'gates/dev-exit'."""
346
343
  green = self._get_phase(tdd_yaml, "green")
347
344
  gate = green.get("gate", {})
348
- assert gate.get("file") == "gates/tests-pass", (
349
- f"Expected gate.file='gates/tests-pass', got gate={gate}"
345
+ assert gate.get("file") == "gates/dev-exit", (
346
+ f"Expected gate.file='gates/dev-exit', got gate={gate}"
350
347
  )
351
348
 
352
- def test_green_phase_keeps_legacy_type(self, tdd_yaml: dict) -> None:
353
- """AC3: Green phase should keep gate.type = 'tests_pass' for backward compat."""
349
+ def test_green_phase_has_dev_exit_type(self, tdd_yaml: dict) -> None:
350
+ """AC3: Green phase should have gate.type = 'dev_exit'."""
354
351
  green = self._get_phase(tdd_yaml, "green")
355
352
  gate = green.get("gate", {})
356
- assert gate.get("type") == "tests_pass", (
357
- f"Expected gate.type='tests_pass', got gate={gate}"
353
+ assert gate.get("type") == "dev_exit", (
354
+ f"Expected gate.type='dev_exit', got gate={gate}"
358
355
  )
359
356
 
360
357
  def test_green_phase_file_is_relative(self, tdd_yaml: dict) -> None:
361
- """AC3: File path should be relative (gates/tests-pass), not absolute."""
358
+ """AC3: File path should be relative (gates/dev-exit), not absolute."""
362
359
  green = self._get_phase(tdd_yaml, "green")
363
360
  gate = green.get("gate", {})
364
361
  file_path = gate.get("file", "")
365
362
  assert not file_path.startswith("/"), (
366
363
  f"gate.file should be relative, got: {file_path}"
367
364
  )
368
- assert file_path == "gates/tests-pass", (
369
- f"Expected 'gates/tests-pass', got: {file_path}"
365
+ assert file_path == "gates/dev-exit", (
366
+ f"Expected 'gates/dev-exit', got: {file_path}"
370
367
  )
371
368
 
372
- def test_red_phase_unchanged(self, tdd_yaml: dict) -> None:
373
- """AC3: Red phase should NOT have gate.file (backward compat period)."""
369
+ def test_red_phase_has_gate_file(self, tdd_yaml: dict) -> None:
370
+ """AC3: Red phase should have gate.file = 'gates/tests-fail'."""
374
371
  red = self._get_phase(tdd_yaml, "red")
375
372
  gate = red.get("gate", {})
376
- assert "file" not in gate, (
377
- f"Red phase should not have gate.file yet, got gate={gate}"
373
+ assert gate.get("file") == "gates/tests-fail", (
374
+ f"Expected gate.file='gates/tests-fail', got gate={gate}"
378
375
  )
379
376
 
380
- def test_review_phase_unchanged(self, tdd_yaml: dict) -> None:
381
- """AC3: Review phase should NOT have gate.file (backward compat period)."""
377
+ def test_review_phase_has_gate_file(self, tdd_yaml: dict) -> None:
378
+ """AC3: Review phase should have gate.file = 'gates/approval'."""
382
379
  review = self._get_phase(tdd_yaml, "review")
383
380
  gate = review.get("gate", {})
384
- assert "file" not in gate, (
385
- f"Review phase should not have gate.file yet, got gate={gate}"
381
+ assert gate.get("file") == "gates/approval", (
382
+ f"Expected gate.file='gates/approval', got gate={gate}"
386
383
  )
387
384
 
388
- def test_setup_phase_unchanged(self, tdd_yaml: dict) -> None:
389
- """AC3: Setup phase should remain gateless."""
385
+ def test_setup_phase_has_gate(self, tdd_yaml: dict) -> None:
386
+ """AC3: Setup phase should have gate.file = 'gates/sm-setup-exit'."""
390
387
  setup = self._get_phase(tdd_yaml, "setup")
391
- assert "gate" not in setup, (
392
- f"Setup phase should not have a gate, got: {setup}"
388
+ gate = setup.get("gate", {})
389
+ assert gate.get("file") == "gates/sm-setup-exit", (
390
+ f"Expected gate.file='gates/sm-setup-exit', got gate={gate}"
393
391
  )
394
392
 
395
393
  def test_finish_phase_unchanged(self, tdd_yaml: dict) -> None:
@@ -435,30 +433,30 @@ class TestResolveGateWithRealTddYaml:
435
433
  def test_green_phase_returns_gate_file(
436
434
  self, real_project: Path
437
435
  ) -> None:
438
- """AC3: resolve_gate for tdd/green should return gate_file='gates/tests-pass'."""
436
+ """AC3: resolve_gate for tdd/green should return gate_file='gates/dev-exit'."""
439
437
  result = resolve_gate(
440
438
  "106-3", "tdd", "green", project_root=real_project
441
439
  )
442
- assert result["gate_file"] == "gates/tests-pass", (
443
- f"Expected gate_file='gates/tests-pass', got: {result}"
440
+ assert result["gate_file"] == "gates/dev-exit", (
441
+ f"Expected gate_file='gates/dev-exit', got: {result}"
444
442
  )
445
443
 
446
- def test_green_phase_still_returns_gate_type(
444
+ def test_green_phase_returns_dev_exit_type(
447
445
  self, real_project: Path
448
446
  ) -> None:
449
- """AC3: resolve_gate for tdd/green should still return gate_type='tests_pass'."""
447
+ """AC3: resolve_gate for tdd/green should return gate_type='dev_exit'."""
450
448
  result = resolve_gate(
451
449
  "106-3", "tdd", "green", project_root=real_project
452
450
  )
453
- assert result["gate_type"] == "tests_pass", (
454
- f"Expected gate_type='tests_pass', got: {result}"
451
+ assert result["gate_type"] == "dev_exit", (
452
+ f"Expected gate_type='dev_exit', got: {result}"
455
453
  )
456
454
 
457
- def test_red_phase_gate_file_is_none(
455
+ def test_red_phase_gate_file_is_tests_fail(
458
456
  self, real_project: Path
459
457
  ) -> None:
460
- """AC3: resolve_gate for tdd/red should have gate_file=None (not migrated yet)."""
458
+ """AC3: resolve_gate for tdd/red should have gate_file='gates/tests-fail'."""
461
459
  result = resolve_gate(
462
460
  "106-3", "tdd", "red", project_root=real_project
463
461
  )
464
- assert result["gate_file"] is None
462
+ assert result["gate_file"] == "gates/tests-fail"
@@ -17,11 +17,7 @@ workflow list team indicator are implemented.
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- from pathlib import Path
21
- from unittest.mock import patch
22
-
23
20
  import pytest
24
- import yaml
25
21
  from click.testing import CliRunner
26
22
 
27
23
  from pennyfarthing_scripts.cli import cli