Skip to content

Commit 1126aa5

Browse files
Merge pull request #207 from Priivacy-ai/codex/fix-workflow-review-lane-gate-1x-20260227
fix(workflow): require for_review lane before starting review
2 parents 2290e3e + 8c1b701 commit 1126aa5

File tree

2 files changed

+101
-2
lines changed

2 files changed

+101
-2
lines changed

src/specify_cli/cli/commands/agent/workflow.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -875,8 +875,16 @@ def review(
875875
# Load work package
876876
wp = locate_work_package(repo_root, feature_slug, normalized_wp_id)
877877

878-
# Move to "doing" lane if not already there
879-
current_lane = extract_scalar(wp.frontmatter, "lane") or "for_review"
878+
# Move to "doing" lane if not already there.
879+
# Explicit WP review requests must target for_review (or already-claimed doing).
880+
current_lane_raw = extract_scalar(wp.frontmatter, "lane") or "for_review"
881+
current_lane = "doing" if current_lane_raw == "in_progress" else current_lane_raw
882+
if current_lane not in {"for_review", "doing"}:
883+
print(f"Error: {normalized_wp_id} is in lane '{current_lane_raw}', not 'for_review'.")
884+
print("Only work packages in 'for_review' can start workflow review.")
885+
print(f"Move it first: spec-kitty agent tasks move-task {normalized_wp_id} --to for_review")
886+
raise typer.Exit(1)
887+
880888
if current_lane != "doing":
881889
# Require --agent parameter to track who is reviewing
882890
if not agent:
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""Regression tests for workflow review lane gating."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
7+
import pytest
8+
from typer.testing import CliRunner
9+
10+
from specify_cli.cli.commands.agent import workflow
11+
from specify_cli.frontmatter import write_frontmatter
12+
from specify_cli.tasks_support import extract_scalar, split_frontmatter
13+
14+
15+
def _write_wp_file(path: Path, wp_id: str, lane: str) -> None:
16+
frontmatter = {
17+
"work_package_id": wp_id,
18+
"subtasks": ["T001"],
19+
"title": f"{wp_id} Test",
20+
"phase": "Phase 0",
21+
"lane": lane,
22+
"assignee": "",
23+
"agent": "",
24+
"shell_pid": "",
25+
"review_status": "",
26+
"reviewed_by": "",
27+
"dependencies": [],
28+
"history": [
29+
{
30+
"timestamp": "2026-01-01T00:00:00Z",
31+
"lane": lane,
32+
"agent": "system",
33+
"shell_pid": "",
34+
"action": "Prompt created",
35+
}
36+
],
37+
}
38+
body = f"# {wp_id} Prompt\n\n## Activity Log\n- 2026-01-01T00:00:00Z – system – lane={lane} – Prompt created.\n"
39+
write_frontmatter(path, frontmatter, body)
40+
41+
42+
@pytest.fixture()
43+
def workflow_repo(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
44+
repo_root = tmp_path
45+
(repo_root / ".kittify").mkdir()
46+
monkeypatch.setenv("SPECIFY_REPO_ROOT", str(repo_root))
47+
monkeypatch.chdir(repo_root)
48+
monkeypatch.setattr(
49+
"specify_cli.cli.commands.agent.workflow._ensure_target_branch_checked_out",
50+
lambda repo_root, feature_slug: (repo_root, "main"),
51+
)
52+
monkeypatch.setattr(
53+
"specify_cli.cli.commands.agent.workflow.safe_commit",
54+
lambda **kwargs: True,
55+
)
56+
return repo_root
57+
58+
59+
def test_workflow_review_rejects_planned_lane(workflow_repo: Path) -> None:
60+
feature_slug = "001-test-feature"
61+
tasks_dir = workflow_repo / "kitty-specs" / feature_slug / "tasks"
62+
tasks_dir.mkdir(parents=True)
63+
wp_path = tasks_dir / "WP01-test.md"
64+
_write_wp_file(wp_path, "WP01", lane="planned")
65+
66+
result = CliRunner().invoke(
67+
workflow.app,
68+
["review", "WP01", "--feature", feature_slug, "--agent", "test-reviewer"],
69+
)
70+
71+
assert result.exit_code == 1
72+
assert "not 'for_review'" in result.stdout
73+
frontmatter, _, _ = split_frontmatter(wp_path.read_text(encoding="utf-8"))
74+
assert extract_scalar(frontmatter, "lane") == "planned"
75+
76+
77+
def test_workflow_review_accepts_for_review_lane(workflow_repo: Path) -> None:
78+
feature_slug = "001-test-feature"
79+
tasks_dir = workflow_repo / "kitty-specs" / feature_slug / "tasks"
80+
tasks_dir.mkdir(parents=True)
81+
wp_path = tasks_dir / "WP01-test.md"
82+
_write_wp_file(wp_path, "WP01", lane="for_review")
83+
84+
result = CliRunner().invoke(
85+
workflow.app,
86+
["review", "WP01", "--feature", feature_slug, "--agent", "test-reviewer"],
87+
)
88+
89+
assert result.exit_code == 0
90+
frontmatter, _, _ = split_frontmatter(wp_path.read_text(encoding="utf-8"))
91+
assert extract_scalar(frontmatter, "lane") == "doing"

0 commit comments

Comments
 (0)