From c80d237e361da353bf1bd5846faa45ae2cc8b700 Mon Sep 17 00:00:00 2001 From: PeninsulaInd Date: Fri, 27 Feb 2026 15:58:26 -0600 Subject: [PATCH] Fix 2: Store OutlinePath in ClickUp custom field for Phase 2 retrieval Phase 1 now writes OutlinePath to a ClickUp custom field via set_custom_field_by_name(). Phase 2 reads it back with get_custom_field_by_name(), falling back to convention path ({outline_dir}/{slug}/outline.md) if the field is empty. Added get_task(), set_custom_field_by_name(), and get_custom_field_by_name() helpers to ClickUpClient. Co-Authored-By: Claude Opus 4.6 --- cheddahbot/clickup.py | 50 ++++++++++++++++++++++++++++ cheddahbot/tools/content_creation.py | 47 ++++++++++++++++++++++++-- 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/cheddahbot/clickup.py b/cheddahbot/clickup.py index 3f302e6..dff6bc7 100644 --- a/cheddahbot/clickup.py +++ b/cheddahbot/clickup.py @@ -419,6 +419,56 @@ class ClickUpClient: log.info("Created custom field '%s' (%s) on list %s", name, field_type, list_id) return result + def get_task(self, task_id: str) -> ClickUpTask: + """Fetch a single task by ID.""" + resp = self._client.get(f"/task/{task_id}") + resp.raise_for_status() + return ClickUpTask.from_api(resp.json(), self._task_type_field_name) + + def set_custom_field_by_name( + self, task_id: str, field_name: str, value: Any + ) -> bool: + """Set a custom field by its human-readable name. + + Looks up the field ID from the task's list, then sets the value. + Falls back gracefully if the field doesn't exist. + """ + try: + task_data = self._client.get(f"/task/{task_id}").json() + list_id = task_data.get("list", {}).get("id", "") + if not list_id: + log.warning("Could not determine list_id for task %s", task_id) + return False + + fields = self.get_custom_fields(list_id) + field_id = None + for f in fields: + if f.get("name") == field_name: + field_id = f["id"] + break + + if not field_id: + log.warning("Field '%s' not found in list %s", field_name, list_id) + return False + + return self.set_custom_field_value(task_id, field_id, value) + except Exception as e: + log.error("Failed to set field '%s' on task %s: %s", field_name, task_id, e) + return False + + def get_custom_field_by_name(self, task_id: str, field_name: str) -> Any: + """Read a custom field value from a task by field name. + + Fetches the task and looks up the field value from custom_fields. + Returns None if not found. + """ + try: + task = self.get_task(task_id) + return task.custom_fields.get(field_name) + except Exception as e: + log.warning("Failed to read field '%s' from task %s: %s", field_name, task_id, e) + return None + def discover_field_filter(self, list_id: str, field_name: str) -> dict[str, Any] | None: """Discover a custom field's UUID and dropdown option map. diff --git a/cheddahbot/tools/content_creation.py b/cheddahbot/tools/content_creation.py index 646cc41..f2aafae 100644 --- a/cheddahbot/tools/content_creation.py +++ b/cheddahbot/tools/content_creation.py @@ -66,13 +66,16 @@ def _sync_clickup_start(ctx: dict | None, task_id: str) -> None: def _sync_clickup_outline_ready(ctx: dict | None, task_id: str, outline_path: str) -> None: - """Post outline comment and move ClickUp task to 'outline review'.""" + """Post outline comment, set OutlinePath field, and move to 'outline review'.""" if not task_id or not ctx: return client = _get_clickup_client(ctx) if not client: return try: + # Store OutlinePath in ClickUp custom field for Phase 2 retrieval + client.set_custom_field_by_name(task_id, "OutlinePath", outline_path) + client.add_comment( task_id, f"📝 CheddahBot generated a content outline.\n\n" @@ -541,6 +544,40 @@ def _run_phase1( # --------------------------------------------------------------------------- +def _resolve_outline_path(ctx: dict | None, task_id: str, keyword: str, config) -> str: + """Resolve the outline path from ClickUp field or convention. + + Priority: ClickUp OutlinePath field → convention path → empty string. + """ + # Try ClickUp custom field first + if task_id and ctx: + client = _get_clickup_client(ctx) + if client: + try: + outline_path = client.get_custom_field_by_name(task_id, "OutlinePath") + if outline_path and str(outline_path).strip(): + return str(outline_path).strip() + except Exception as e: + log.warning("Failed to read OutlinePath from ClickUp for %s: %s", task_id, e) + finally: + client.close() + + # Fallback to convention path + slug = _slugify(keyword) + if slug and config and config.content.outline_dir: + convention_path = Path(config.content.outline_dir) / slug / "outline.md" + if convention_path.exists(): + return str(convention_path) + + # Try local fallback too + if slug: + local_path = _LOCAL_CONTENT_DIR / slug / "outline.md" + if local_path.exists(): + return str(local_path) + + return "" + + def _run_phase2( *, agent, @@ -556,8 +593,12 @@ def _run_phase2( is_service_page: bool = False, capabilities_default: str = "", ) -> str: - # Read the (possibly edited) outline - outline_path = existing_state.get("outline_path", "") + # Resolve outline path: ClickUp field → convention → state fallback + outline_path = _resolve_outline_path(ctx, task_id, keyword, config) + if not outline_path: + # Last resort: check existing_state (for continue_content calls) + outline_path = existing_state.get("outline_path", "") + outline_text = "" if outline_path: try: