fix(consistency_check): heading titles + workstream-aware task guards
- parse_task_blocks() now injects the nearest preceding ### heading text as `title` — tasks no longer stored with bare IDs as their title - C-11 fix skips creating tasks when workstream is completed/archived (prevents duplicate task creation on repeated fix-consistency runs) - C-12 is now fixable: auto-cancels open orphan DB tasks when the backing workstream is finished (completed/archived) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,7 @@ except ImportError:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
_TASK_BLOCK_RE = re.compile(r"```task\s*\n(.*?)\n```", re.DOTALL)
|
_TASK_BLOCK_RE = re.compile(r"```task\s*\n(.*?)\n```", re.DOTALL)
|
||||||
|
_HEADING_RE = re.compile(r"^#{1,4}\s+(.+?)$", re.MULTILINE)
|
||||||
VALID_WP_STATUSES = {"active", "completed", "archived"}
|
VALID_WP_STATUSES = {"active", "completed", "archived"}
|
||||||
VALID_TASK_STATUSES = {"todo", "in_progress", "blocked", "done", "cancelled"}
|
VALID_TASK_STATUSES = {"todo", "in_progress", "blocked", "done", "cancelled"}
|
||||||
VALID_TASK_PRIORITIES = {"low", "medium", "high", "critical"}
|
VALID_TASK_PRIORITIES = {"low", "medium", "high", "critical"}
|
||||||
@@ -148,8 +149,17 @@ def parse_frontmatter(text: str) -> tuple[dict, str]:
|
|||||||
|
|
||||||
|
|
||||||
def parse_task_blocks(body: str) -> list[dict]:
|
def parse_task_blocks(body: str) -> list[dict]:
|
||||||
"""Extract all ```task ... ``` YAML blocks from a workplan body."""
|
"""Extract task blocks, injecting title from the nearest preceding ### heading."""
|
||||||
return [_parse_yaml_block(m.group(1).strip()) for m in _TASK_BLOCK_RE.finditer(body)]
|
headings = [(m.start(), m.group(1).strip()) for m in _HEADING_RE.finditer(body)]
|
||||||
|
results = []
|
||||||
|
for m in _TASK_BLOCK_RE.finditer(body):
|
||||||
|
task = _parse_yaml_block(m.group(1).strip())
|
||||||
|
if "title" not in task:
|
||||||
|
prev = [(pos, text) for pos, text in headings if pos < m.start()]
|
||||||
|
if prev:
|
||||||
|
task["title"] = prev[-1][1]
|
||||||
|
results.append(task)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def get_tasks_from_workplan(meta: dict, body: str) -> list[dict]:
|
def get_tasks_from_workplan(meta: dict, body: str) -> list[dict]:
|
||||||
@@ -524,6 +534,7 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N
|
|||||||
)
|
)
|
||||||
elif t_id:
|
elif t_id:
|
||||||
# C-11: task exists in file but not linked to DB
|
# C-11: task exists in file but not linked to DB
|
||||||
|
ws_status = ws.get("status", "")
|
||||||
report.add(
|
report.add(
|
||||||
severity="WARN", check_id="C-11",
|
severity="WARN", check_id="C-11",
|
||||||
message=f"Task '{t_id}' has no state_hub_task_id",
|
message=f"Task '{t_id}' has no state_hub_task_id",
|
||||||
@@ -531,6 +542,7 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N
|
|||||||
fixable=True,
|
fixable=True,
|
||||||
_fix_context={
|
_fix_context={
|
||||||
"ws_id": ws_id,
|
"ws_id": ws_id,
|
||||||
|
"ws_status": ws_status,
|
||||||
"task": task,
|
"task": task,
|
||||||
"wp_file": str(wp_file),
|
"wp_file": str(wp_file),
|
||||||
"meta": meta,
|
"meta": meta,
|
||||||
@@ -540,17 +552,27 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N
|
|||||||
|
|
||||||
# C-12: DB tasks with no file backing
|
# C-12: DB tasks with no file backing
|
||||||
if isinstance(db_tasks, list):
|
if isinstance(db_tasks, list):
|
||||||
|
ws_status = ws.get("status", "")
|
||||||
|
ws_finished = ws_status in ("completed", "archived")
|
||||||
for db_t in db_tasks:
|
for db_t in db_tasks:
|
||||||
if db_t["id"] not in file_task_sh_ids:
|
if db_t["id"] not in file_task_sh_ids:
|
||||||
|
db_t_status = db_t.get("status", "")
|
||||||
|
open_task = db_t_status not in ("done", "cancelled")
|
||||||
|
# Auto-cancel fixable when workstream is finished and task is open
|
||||||
|
fixable = ws_finished and open_task
|
||||||
report.add(
|
report.add(
|
||||||
severity="WARN", check_id="C-12",
|
severity="WARN", check_id="C-12",
|
||||||
message=(
|
message=(
|
||||||
f"DB task '{db_t.get('title', '')}' "
|
f"DB task '{db_t.get('title', '')}' "
|
||||||
f"(id={db_t['id'][:8]}…, status={db_t.get('status', '')}) "
|
f"(id={db_t['id'][:8]}…, status={db_t_status}) "
|
||||||
f"in workstream '{ws.get('slug')}' has no file backing"
|
f"in workstream '{ws.get('slug')}' has no file backing"
|
||||||
),
|
),
|
||||||
db_id=db_t["id"],
|
db_id=db_t["id"],
|
||||||
fixable=False,
|
fixable=fixable,
|
||||||
|
_fix_context={
|
||||||
|
"task_id": db_t["id"],
|
||||||
|
"ws_finished": ws_finished,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# C-13: all DB tasks done but workstream still active — worker forgot to close
|
# C-13: all DB tasks done but workstream still active — worker forgot to close
|
||||||
@@ -810,34 +832,51 @@ def fix_repo(api_base: str, repo_slug: str, repo_path_override: str | None = Non
|
|||||||
|
|
||||||
elif issue.check_id == "C-11":
|
elif issue.check_id == "C-11":
|
||||||
ws_id = ctx["ws_id"]
|
ws_id = ctx["ws_id"]
|
||||||
|
ws_status = ctx.get("ws_status", "")
|
||||||
task = ctx["task"]
|
task = ctx["task"]
|
||||||
wp_file = Path(ctx["wp_file"])
|
wp_file = Path(ctx["wp_file"])
|
||||||
meta = ctx["meta"]
|
meta = ctx["meta"]
|
||||||
body = ctx.get("body", "")
|
body = ctx.get("body", "")
|
||||||
t_id = str(task.get("id", "")).strip()
|
t_id = str(task.get("id", "")).strip()
|
||||||
t_status = str(task.get("status", "todo")).strip()
|
# Skip creating tasks for finished workstreams — the workstream is
|
||||||
if t_status not in VALID_TASK_STATUSES:
|
# done/archived so unlinked tasks are stale file artefacts, not gaps.
|
||||||
t_status = "todo"
|
if ws_status in ("completed", "archived"):
|
||||||
t_priority = str(task.get("priority", "medium")).strip()
|
|
||||||
if t_priority not in VALID_TASK_PRIORITIES:
|
|
||||||
t_priority = "medium"
|
|
||||||
t_data = _api_post(api_base, "/tasks", {
|
|
||||||
"workstream_id": ws_id,
|
|
||||||
"title": str(task.get("title", t_id)).strip() or t_id,
|
|
||||||
"status": t_status,
|
|
||||||
"priority": t_priority,
|
|
||||||
"assignee": task.get("assignee") or None,
|
|
||||||
})
|
|
||||||
if t_data:
|
|
||||||
t_db_id = t_data["id"]
|
|
||||||
injected = _inject_task_id_into_block(
|
|
||||||
wp_file, "state_hub_task_id", t_db_id, t_id
|
|
||||||
)
|
|
||||||
if not injected:
|
|
||||||
_inject_task_id_frontmatter_list(wp_file, t_db_id, t_id)
|
|
||||||
report.fixes_applied.append(
|
report.fixes_applied.append(
|
||||||
f"C-11 fixed: task '{t_id}' → {t_db_id[:8]}…"
|
f"C-11 skipped: task '{t_id}' in {ws_status} workstream — not created"
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
t_status = str(task.get("status", "todo")).strip()
|
||||||
|
if t_status not in VALID_TASK_STATUSES:
|
||||||
|
t_status = "todo"
|
||||||
|
t_priority = str(task.get("priority", "medium")).strip()
|
||||||
|
if t_priority not in VALID_TASK_PRIORITIES:
|
||||||
|
t_priority = "medium"
|
||||||
|
t_data = _api_post(api_base, "/tasks", {
|
||||||
|
"workstream_id": ws_id,
|
||||||
|
"title": str(task.get("title", t_id)).strip() or t_id,
|
||||||
|
"status": t_status,
|
||||||
|
"priority": t_priority,
|
||||||
|
"assignee": task.get("assignee") or None,
|
||||||
|
})
|
||||||
|
if t_data:
|
||||||
|
t_db_id = t_data["id"]
|
||||||
|
injected = _inject_task_id_into_block(
|
||||||
|
wp_file, "state_hub_task_id", t_db_id, t_id
|
||||||
|
)
|
||||||
|
if not injected:
|
||||||
|
_inject_task_id_frontmatter_list(wp_file, t_db_id, t_id)
|
||||||
|
report.fixes_applied.append(
|
||||||
|
f"C-11 fixed: task '{t_id}' → {t_db_id[:8]}…"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif issue.check_id == "C-12":
|
||||||
|
task_id = ctx["task_id"]
|
||||||
|
if ctx.get("ws_finished"):
|
||||||
|
result = _api_patch(api_base, f"/tasks/{task_id}", {"status": "cancelled"})
|
||||||
|
if result is not None:
|
||||||
|
report.fixes_applied.append(
|
||||||
|
f"C-12 fixed: orphan task {task_id[:8]}… cancelled (workstream finished)"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
report.fixes_applied.append(f"{issue.check_id} ERROR: {e}")
|
report.fixes_applied.append(f"{issue.check_id} ERROR: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user