Skip to content

Commit 30cc722

Browse files
fix(specs): _STATUS_VALUE_RE accepts both **Status**: and **Status:** (#57)
* fix(specs): _STATUS_VALUE_RE accepts both **Status**: and **Status:** The regex previously only matched `**Status**:` (colon outside asterisks). Real specs in this repo overwhelmingly use `**Status:**` (colon inside) — 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. _STATUS_RE — old: r"^\s*\*\*Status\*\*:.*$" — new: r"^\s*\*\*Status(?:\*\*:|:\*\*).*$" _STATUS_VALUE_RE — old: r"\*\*Status\*\*:\s*(\S+)" — new: r"\*\*Status(?:\*\*:|:\*\*)\s*(\S+)" The alternation accepts either order of the closing asterisks and colon. Malformed look-alikes (`**Status:`, `**Status: foo` without closing asterisks, `**Statuss**: foo`, `## Status: foo`) still get correctly rejected. Tests: - test_cowork_specs.py: • test_status_regex_accepts_both_markdown_emphasis_styles (4 parametrized happy paths covering both formats, the dashed-phrase form, and indented lines) • test_status_regex_rejects_malformed_lines (4 parametrized negative cases) - test_mcp_tools.py: • test_get_spec_reads_colon_inside_status_format — covers the MCP path, not just the FastAPI route The PUT-status route's substitution still rewrites in the `**Status**:` form; that's a separate normalization choice and isn't changed here. Surfaced during PR #52's MCP Phase 2 work and flagged via a follow-up chip; this closes the chip's intent. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(living-docs): regenerate sidecar templates for status-regex fix source_hash drift only — no narrative change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d282f21 commit 30cc722

7 files changed

Lines changed: 113 additions & 8 deletions

File tree

.help/templates/sidecar/concept.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
---
22
feature: sidecar
33
depth: concept
4-
generated_at: 2026-05-23T15:26:20.582683+00:00
5-
source_hash: 278c82dcb0488307aba1af37c2e2b49ddd2ba1b93775befbe9147bf96965936d
4+
generated_at: 2026-05-23T15:38:15.234638+00:00
5+
source_hash: a1d71c4c13ab81ddb75e5a3c1d6096e69128d047613aaee8b3ccf4f65d356a74
66
status: generated
77
---
88

.help/templates/sidecar/reference.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
---
22
feature: sidecar
33
depth: reference
4-
generated_at: 2026-05-23T15:26:20.594051+00:00
5-
source_hash: 278c82dcb0488307aba1af37c2e2b49ddd2ba1b93775befbe9147bf96965936d
4+
generated_at: 2026-05-23T15:38:15.246052+00:00
5+
source_hash: a1d71c4c13ab81ddb75e5a3c1d6096e69128d047613aaee8b3ccf4f65d356a74
66
status: generated
77
---
88

@@ -271,6 +271,8 @@ status: generated
271271
| `test_specs_skips_dot_dirs()` || `sidecar/tests/test_cowork_specs.py` |
272272
| `test_specs_returns_empty_when_no_root()` || `sidecar/tests/test_cowork_specs.py` |
273273
| `test_spec_with_no_phase_files_handled()` || `sidecar/tests/test_cowork_specs.py` |
274+
| `test_status_regex_accepts_both_markdown_emphasis_styles()` | Pre-fix bug: _STATUS_VALUE_RE only matched ``**Status**:`` (colon | `sidecar/tests/test_cowork_specs.py` |
275+
| `test_status_regex_rejects_malformed_lines()` | The looser regex must still reject lines that aren't valid | `sidecar/tests/test_cowork_specs.py` |
274276
| `test_specs_root_env_var_wins()` || `sidecar/tests/test_cowork_specs.py` |
275277
| `test_specs_root_falls_back_to_workspace()` || `sidecar/tests/test_cowork_specs.py` |
276278
| `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` |
@@ -541,6 +543,7 @@ status: generated
541543
| `test_get_spec_returns_phase_contents()` || `sidecar/tests/test_mcp_tools.py` |
542544
| `test_get_spec_rejects_invalid_slug()` || `sidecar/tests/test_mcp_tools.py` |
543545
| `test_get_spec_unknown_feature_errors()` || `sidecar/tests/test_mcp_tools.py` |
546+
| `test_get_spec_reads_colon_inside_status_format()` | Confidence test for the loosened ``_STATUS_VALUE_RE`` — the common | `sidecar/tests/test_mcp_tools.py` |
544547
| `test_get_spec_status_returns_most_advanced()` || `sidecar/tests/test_mcp_tools.py` |
545548
| `test_get_spec_status_explicit_phase()` || `sidecar/tests/test_mcp_tools.py` |
546549
| `test_get_spec_status_rejects_invalid_phase()` || `sidecar/tests/test_mcp_tools.py` |

.help/templates/sidecar/task.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
---
22
feature: sidecar
33
depth: task
4-
generated_at: 2026-05-23T15:26:20.589168+00:00
5-
source_hash: 278c82dcb0488307aba1af37c2e2b49ddd2ba1b93775befbe9147bf96965936d
4+
generated_at: 2026-05-23T15:38:15.241623+00:00
5+
source_hash: a1d71c4c13ab81ddb75e5a3c1d6096e69128d047613aaee8b3ccf4f65d356a74
66
status: generated
77
---
88

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
55

66
## [Unreleased]
77

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

1026
- **MCP server — Phase 5 docs.** New "## MCP integration" section

sidecar/attune_gui/routes/cowork_specs.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,15 @@
3535

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

38-
_STATUS_RE = re.compile(r"^\s*\*\*Status\*\*:.*$", re.MULTILINE)
39-
_STATUS_VALUE_RE = re.compile(r"\*\*Status\*\*:\s*(\S+)")
38+
# Match both `**Status**:` (colon outside asterisks) and `**Status:**`
39+
# (colon inside asterisks). Both are valid markdown for the same intent
40+
# and both appear in the wild across specs in this repo — `**Status:**`
41+
# is actually the more common form. The old regex only matched the
42+
# colon-outside variant, silently returning ``None`` for ~7 of every 8
43+
# specs in the dashboard's Status column and in the MCP
44+
# ``gui_get_spec_status`` tool.
45+
_STATUS_RE = re.compile(r"^\s*\*\*Status(?:\*\*:|:\*\*).*$", re.MULTILINE)
46+
_STATUS_VALUE_RE = re.compile(r"\*\*Status(?:\*\*:|:\*\*)\s*(\S+)")
4047
_PHASE_FILES = ("requirements.md", "design.md", "tasks.md")
4148
_PHASE_LABELS = {
4249
"requirements.md": "Requirements",

sidecar/tests/test_cowork_specs.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,64 @@ def test_spec_with_no_phase_files_handled(
114114
assert s["status"] is None
115115

116116

117+
@pytest.mark.parametrize(
118+
("source", "expected"),
119+
[
120+
("# Spec\n\n**Status**: approved\n", "approved"), # colon outside
121+
("# Spec\n\n**Status:** approved\n", "approved"), # colon inside (the common form)
122+
("# Spec\n\n**Status:** Closed — final\n", "Closed"), # dashed phrase, common in real specs
123+
("# Spec\n **Status:** pending\n", "pending"), # indented
124+
],
125+
)
126+
def test_status_regex_accepts_both_markdown_emphasis_styles(
127+
client: TestClient,
128+
monkeypatch: pytest.MonkeyPatch,
129+
tmp_path: Path,
130+
source: str,
131+
expected: str,
132+
) -> None:
133+
"""Pre-fix bug: _STATUS_VALUE_RE only matched ``**Status**:`` (colon
134+
outside). Real specs in the repo overwhelmingly use ``**Status:**``
135+
(colon inside), which was silently returning ``None``. Both forms
136+
must now parse to the same status value."""
137+
specs_root = tmp_path / "specs"
138+
feat = specs_root / "alpha"
139+
feat.mkdir(parents=True)
140+
(feat / "requirements.md").write_text(source)
141+
142+
monkeypatch.setattr(cowork_specs, "_specs_roots", lambda: [specs_root])
143+
144+
body = client.get("/api/cowork/specs", headers={"Origin": "http://localhost:5173"}).json()
145+
alpha = next(s for s in body["specs"] if s["feature"] == "alpha")
146+
assert alpha["status"] == expected
147+
148+
149+
@pytest.mark.parametrize(
150+
"bad_source",
151+
[
152+
"# Spec\n\n**Status:\n", # missing trailing **:
153+
"# Spec\n\n**Status: approved\n", # missing closing **
154+
"# Spec\n\n**Statuss**: approved\n", # double-s typo
155+
"# Spec\n\n## Status: approved\n", # heading, not bold
156+
],
157+
)
158+
def test_status_regex_rejects_malformed_lines(
159+
client: TestClient, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, bad_source: str
160+
) -> None:
161+
"""The looser regex must still reject lines that aren't valid
162+
bold-Status declarations."""
163+
specs_root = tmp_path / "specs"
164+
feat = specs_root / "alpha"
165+
feat.mkdir(parents=True)
166+
(feat / "requirements.md").write_text(bad_source)
167+
168+
monkeypatch.setattr(cowork_specs, "_specs_roots", lambda: [specs_root])
169+
170+
body = client.get("/api/cowork/specs", headers={"Origin": "http://localhost:5173"}).json()
171+
alpha = next(s for s in body["specs"] if s["feature"] == "alpha")
172+
assert alpha["status"] is None
173+
174+
117175
# ---------------------------------------------------------------------------
118176
# _specs_root resolution
119177
# ---------------------------------------------------------------------------

sidecar/tests/test_mcp_tools.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,27 @@ async def test_get_spec_unknown_feature_errors(app, specs_root) -> None:
112112
assert "'nope' not found" in result["error"]
113113

114114

115+
@pytest.mark.asyncio
116+
async def test_get_spec_reads_colon_inside_status_format(app, tmp_path, monkeypatch) -> None:
117+
"""Confidence test for the loosened ``_STATUS_VALUE_RE`` — the common
118+
``**Status:**`` (colon inside asterisks) format must read correctly
119+
through the MCP path, not just the FastAPI route."""
120+
root = tmp_path / "specs"
121+
feat = root / "colon-inside"
122+
feat.mkdir(parents=True)
123+
# The format that real specs in this repo overwhelmingly use:
124+
(feat / "requirements.md").write_text(
125+
"# colon-inside\n\n**Status:** Closed — done\n", encoding="utf-8"
126+
)
127+
monkeypatch.setenv("ATTUNE_SPECS_ROOT", str(root))
128+
129+
result = await app.call_tool(
130+
"gui_get_spec_status", {"feature": "colon-inside", "phase": "requirements"}
131+
)
132+
assert result["success"] is True
133+
assert result["status"] == "Closed"
134+
135+
115136
# ---------------------------------------------------------------------------
116137
# gui_get_spec_status
117138
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)