Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .help/templates/sidecar/concept.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
feature: sidecar
depth: concept
generated_at: 2026-05-23T15:26:20.582683+00:00
source_hash: 278c82dcb0488307aba1af37c2e2b49ddd2ba1b93775befbe9147bf96965936d
generated_at: 2026-05-23T15:38:15.234638+00:00
source_hash: a1d71c4c13ab81ddb75e5a3c1d6096e69128d047613aaee8b3ccf4f65d356a74
status: generated
---

Expand Down
7 changes: 5 additions & 2 deletions .help/templates/sidecar/reference.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
feature: sidecar
depth: reference
generated_at: 2026-05-23T15:26:20.594051+00:00
source_hash: 278c82dcb0488307aba1af37c2e2b49ddd2ba1b93775befbe9147bf96965936d
generated_at: 2026-05-23T15:38:15.246052+00:00
source_hash: a1d71c4c13ab81ddb75e5a3c1d6096e69128d047613aaee8b3ccf4f65d356a74
status: generated
---

Expand Down Expand Up @@ -271,6 +271,8 @@ status: generated
| `test_specs_skips_dot_dirs()` | — | `sidecar/tests/test_cowork_specs.py` |
| `test_specs_returns_empty_when_no_root()` | — | `sidecar/tests/test_cowork_specs.py` |
| `test_spec_with_no_phase_files_handled()` | — | `sidecar/tests/test_cowork_specs.py` |
| `test_status_regex_accepts_both_markdown_emphasis_styles()` | Pre-fix bug: _STATUS_VALUE_RE only matched ``**Status**:`` (colon | `sidecar/tests/test_cowork_specs.py` |
| `test_status_regex_rejects_malformed_lines()` | The looser regex must still reject lines that aren't valid | `sidecar/tests/test_cowork_specs.py` |
| `test_specs_root_env_var_wins()` | — | `sidecar/tests/test_cowork_specs.py` |
| `test_specs_root_falls_back_to_workspace()` | — | `sidecar/tests/test_cowork_specs.py` |
| `test_specs_root_falls_back_to_workspace_docs_specs()` | Workspaces that keep specs at ``docs/specs/`` (attune-rag, attune-ai layout) | `sidecar/tests/test_cowork_specs.py` |
Expand Down Expand Up @@ -541,6 +543,7 @@ status: generated
| `test_get_spec_returns_phase_contents()` | — | `sidecar/tests/test_mcp_tools.py` |
| `test_get_spec_rejects_invalid_slug()` | — | `sidecar/tests/test_mcp_tools.py` |
| `test_get_spec_unknown_feature_errors()` | — | `sidecar/tests/test_mcp_tools.py` |
| `test_get_spec_reads_colon_inside_status_format()` | Confidence test for the loosened ``_STATUS_VALUE_RE`` — the common | `sidecar/tests/test_mcp_tools.py` |
| `test_get_spec_status_returns_most_advanced()` | — | `sidecar/tests/test_mcp_tools.py` |
| `test_get_spec_status_explicit_phase()` | — | `sidecar/tests/test_mcp_tools.py` |
| `test_get_spec_status_rejects_invalid_phase()` | — | `sidecar/tests/test_mcp_tools.py` |
Expand Down
4 changes: 2 additions & 2 deletions .help/templates/sidecar/task.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
feature: sidecar
depth: task
generated_at: 2026-05-23T15:26:20.589168+00:00
source_hash: 278c82dcb0488307aba1af37c2e2b49ddd2ba1b93775befbe9147bf96965936d
generated_at: 2026-05-23T15:38:15.241623+00:00
source_hash: a1d71c4c13ab81ddb75e5a3c1d6096e69128d047613aaee8b3ccf4f65d356a74
status: generated
---

Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Fixed

- **`**Status:**` (colon-inside-asterisks) format now parses
correctly.** `_STATUS_RE` and `_STATUS_VALUE_RE` in
`routes/cowork_specs.py` previously only matched the
colon-outside form (`**Status**:`). Real specs in this repo
overwhelmingly use `**Status:**` instead — a quick grep showed
~7 of 8 status lines in that form. The dashboard's Status
column and the MCP `gui_get_spec_status` tool were silently
returning `None` for those specs. Loosened both regexes to
accept either emphasis style. Added 8 parametrized tests in
`test_cowork_specs.py` (4 happy paths, 4 malformed-line
rejections) and 1 MCP-path test to lock in both behaviors.
Surfaced during PR #52's MCP Phase 2 work; tracked via the
follow-up chip spawned at the time.

### Added

- **MCP server — Phase 5 docs.** New "## MCP integration" section
Expand Down
11 changes: 9 additions & 2 deletions sidecar/attune_gui/routes/cowork_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,15 @@

router = APIRouter(prefix="/api/cowork", tags=["cowork-specs"])

_STATUS_RE = re.compile(r"^\s*\*\*Status\*\*:.*$", re.MULTILINE)
_STATUS_VALUE_RE = re.compile(r"\*\*Status\*\*:\s*(\S+)")
# Match both `**Status**:` (colon outside asterisks) and `**Status:**`
# (colon inside asterisks). Both are valid markdown for the same intent
# and both appear in the wild across specs in this repo — `**Status:**`
# is actually the more common form. The old regex only matched the
# colon-outside variant, silently returning ``None`` for ~7 of every 8
# specs in the dashboard's Status column and in the MCP
# ``gui_get_spec_status`` tool.
_STATUS_RE = re.compile(r"^\s*\*\*Status(?:\*\*:|:\*\*).*$", re.MULTILINE)
_STATUS_VALUE_RE = re.compile(r"\*\*Status(?:\*\*:|:\*\*)\s*(\S+)")
_PHASE_FILES = ("requirements.md", "design.md", "tasks.md")
_PHASE_LABELS = {
"requirements.md": "Requirements",
Expand Down
58 changes: 58 additions & 0 deletions sidecar/tests/test_cowork_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,64 @@ def test_spec_with_no_phase_files_handled(
assert s["status"] is None


@pytest.mark.parametrize(
("source", "expected"),
[
("# Spec\n\n**Status**: approved\n", "approved"), # colon outside
("# Spec\n\n**Status:** approved\n", "approved"), # colon inside (the common form)
("# Spec\n\n**Status:** Closed — final\n", "Closed"), # dashed phrase, common in real specs
("# Spec\n **Status:** pending\n", "pending"), # indented
],
)
def test_status_regex_accepts_both_markdown_emphasis_styles(
client: TestClient,
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
source: str,
expected: str,
) -> None:
"""Pre-fix bug: _STATUS_VALUE_RE only matched ``**Status**:`` (colon
outside). Real specs in the repo overwhelmingly use ``**Status:**``
(colon inside), which was silently returning ``None``. Both forms
must now parse to the same status value."""
specs_root = tmp_path / "specs"
feat = specs_root / "alpha"
feat.mkdir(parents=True)
(feat / "requirements.md").write_text(source)

monkeypatch.setattr(cowork_specs, "_specs_roots", lambda: [specs_root])

body = client.get("/api/cowork/specs", headers={"Origin": "http://localhost:5173"}).json()
alpha = next(s for s in body["specs"] if s["feature"] == "alpha")
assert alpha["status"] == expected


@pytest.mark.parametrize(
"bad_source",
[
"# Spec\n\n**Status:\n", # missing trailing **:
"# Spec\n\n**Status: approved\n", # missing closing **
"# Spec\n\n**Statuss**: approved\n", # double-s typo
"# Spec\n\n## Status: approved\n", # heading, not bold
],
)
def test_status_regex_rejects_malformed_lines(
client: TestClient, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, bad_source: str
) -> None:
"""The looser regex must still reject lines that aren't valid
bold-Status declarations."""
specs_root = tmp_path / "specs"
feat = specs_root / "alpha"
feat.mkdir(parents=True)
(feat / "requirements.md").write_text(bad_source)

monkeypatch.setattr(cowork_specs, "_specs_roots", lambda: [specs_root])

body = client.get("/api/cowork/specs", headers={"Origin": "http://localhost:5173"}).json()
alpha = next(s for s in body["specs"] if s["feature"] == "alpha")
assert alpha["status"] is None


# ---------------------------------------------------------------------------
# _specs_root resolution
# ---------------------------------------------------------------------------
Expand Down
21 changes: 21 additions & 0 deletions sidecar/tests/test_mcp_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,27 @@ async def test_get_spec_unknown_feature_errors(app, specs_root) -> None:
assert "'nope' not found" in result["error"]


@pytest.mark.asyncio
async def test_get_spec_reads_colon_inside_status_format(app, tmp_path, monkeypatch) -> None:
"""Confidence test for the loosened ``_STATUS_VALUE_RE`` — the common
``**Status:**`` (colon inside asterisks) format must read correctly
through the MCP path, not just the FastAPI route."""
root = tmp_path / "specs"
feat = root / "colon-inside"
feat.mkdir(parents=True)
# The format that real specs in this repo overwhelmingly use:
(feat / "requirements.md").write_text(
"# colon-inside\n\n**Status:** Closed — done\n", encoding="utf-8"
)
monkeypatch.setenv("ATTUNE_SPECS_ROOT", str(root))

result = await app.call_tool(
"gui_get_spec_status", {"feature": "colon-inside", "phase": "requirements"}
)
assert result["success"] is True
assert result["status"] == "Closed"


# ---------------------------------------------------------------------------
# gui_get_spec_status
# ---------------------------------------------------------------------------
Expand Down
Loading