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 <noreply@anthropic.com>
fix/customer-field-migration
PeninsulaInd 2026-02-27 15:58:26 -06:00
parent 5ddeb93033
commit c80d237e36
2 changed files with 94 additions and 3 deletions

View File

@ -419,6 +419,56 @@ class ClickUpClient:
log.info("Created custom field '%s' (%s) on list %s", name, field_type, list_id) log.info("Created custom field '%s' (%s) on list %s", name, field_type, list_id)
return result 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: 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. """Discover a custom field's UUID and dropdown option map.

View File

@ -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: 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: if not task_id or not ctx:
return return
client = _get_clickup_client(ctx) client = _get_clickup_client(ctx)
if not client: if not client:
return return
try: 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( client.add_comment(
task_id, task_id,
f"📝 CheddahBot generated a content outline.\n\n" 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( def _run_phase2(
*, *,
agent, agent,
@ -556,8 +593,12 @@ def _run_phase2(
is_service_page: bool = False, is_service_page: bool = False,
capabilities_default: str = "", capabilities_default: str = "",
) -> str: ) -> str:
# Read the (possibly edited) outline # 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_path = existing_state.get("outline_path", "")
outline_text = "" outline_text = ""
if outline_path: if outline_path:
try: try: