diff --git a/CLAUDE.md b/CLAUDE.md index 67a79d1..caac33e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -102,6 +102,18 @@ Every Claude Code session in this repository must follow this ritual: This syncs task blocks → DB and updates task statuses. Without this step, the "Open Workstreams by Domain" chart will show 0 progress even for completed work. +**Workplan ↔ DB sync rule (prevents ghost workstreams):** +When creating a new workstream backed by a workplan file, **always write the file +first, then run `make fix-consistency`** — never call `create_workstream()` / +`create_task()` manually for file-backed work. Calling the MCP bootstrap tools +before the file exists creates a "ghost" workstream that the consistency checker +cannot see (it has `repo_id=null`). The checker then creates a second workstream +from the file, and the ghost stays active forever showing false partial progress. + +Rule of thumb: +- **Workplan file will be written → file first, then `fix-consistency`** +- **No workplan file (bootstrap / first-session only) → `create_workstream()` is fine** + The state hub is the episodic memory of this system. A session that produces no progress events is invisible to future sessions and to Bernd. ## Governance Constraints diff --git a/state-hub/scripts/consistency_check.py b/state-hub/scripts/consistency_check.py index d7e897b..1790aca 100644 --- a/state-hub/scripts/consistency_check.py +++ b/state-hub/scripts/consistency_check.py @@ -18,6 +18,7 @@ Checks: C-11 task-unlinked WARN Yes Task block has no state_hub_task_id C-12 orphan-db-task WARN No DB task in workstream has no file backing C-13 workstream-auto-complete WARN Yes All DB tasks done but workstream still active + C-14 ghost-duplicate WARN No Active topic workstream with no repo_id matches a file-backed title — probable ghost from premature create_workstream() call Usage: python scripts/consistency_check.py --repo SLUG [--fix] [--json] [--api-base URL] @@ -581,6 +582,12 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N # C-07 / C-08: orphan DB workstreams (have repo_id=this_repo but no backing file) _check_orphan_db(api_base, repo_id, set(file_ws_ids.keys()), report) + # C-14: ghost duplicate — active workstream on same topic with no repo_id whose + # title matches a file-backed workstream. Root cause: create_workstream() called + # before the workplan file was written; fix-consistency then created a second + # workstream from the file, leaving the first as an invisible orphan. + _check_ghost_duplicates(api_base, workplan_infos, file_ws_ids, report) + return report @@ -619,6 +626,62 @@ def _check_orphan_db( ) +def _check_ghost_duplicates( + api_base: str, + workplan_infos: list[tuple], + file_ws_ids: dict[str, tuple], + report: ConsistencyReport, +) -> None: + """C-14: detect active workstreams with repo_id=null whose title matches a + file-backed workstream — these are ghosts created by premature create_workstream() + calls before the workplan file existed. + """ + # Build lookup: normalised title → file-backed workstream id + file_titles: dict[str, str] = {} + for _, meta, _ in workplan_infos: + ws_id = str(meta.get("state_hub_workstream_id", "")).strip().strip('"') + title = str(meta.get("title", "")).strip().lower() + if title and ws_id: + file_titles[title] = ws_id + + if not file_titles: + return + + # Gather topic_ids from all file-backed workstreams so we can query by topic + topic_ids: set[str] = set() + for ws_id in file_ws_ids: + ws = _api_get(api_base, f"/workstreams/{ws_id}") + if ws and ws.get("topic_id"): + topic_ids.add(ws["topic_id"]) + + for topic_id in topic_ids: + topic_ws = _api_get(api_base, "/workstreams", {"topic_id": topic_id, "status": "active"}) + if not isinstance(topic_ws, list): + continue + for ws in topic_ws: + ws_id = ws["id"] + if ws_id in file_ws_ids: + continue # legitimately linked + if ws.get("repo_id"): + continue # C-07 covers repo-scoped orphans + ws_title = ws.get("title", "").strip().lower() + if ws_title in file_titles: + file_backed_id = file_titles[ws_title] + report.add( + severity="WARN", + check_id="C-14", + message=( + f"Ghost duplicate: active workstream '{ws.get('slug')}' " + f"(id={ws_id[:8]}…, repo_id=null) has same title as " + f"file-backed workstream {file_backed_id[:8]}… — " + f"likely created via create_workstream() before workplan file existed; " + f"archive it" + ), + db_id=ws_id, + fixable=False, + ) + + # --------------------------------------------------------------------------- # Fix engine # ---------------------------------------------------------------------------