From 082ca6ba447a0c3c6ce26fb50f50ee3c205e3f2f Mon Sep 17 00:00:00 2001 From: PeninsulaInd Date: Tue, 17 Feb 2026 16:36:45 -0600 Subject: [PATCH] Fix PR task flow: add ClickUp status updates, comments, and attachments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The press release tool now handles its own ClickUp sync lifecycle when a clickup_task_id is provided — sets status to "in progress" with a starting comment, uploads docx attachments after creation, then sets status to "internal review" with a completion comment. The scheduler now passes clickup_task_id to tools and defers to tool-level sync when detected, falling back to scheduler-level sync for other tools. ToolRegistry.execute() now filters args to accepted params to prevent TypeError when extra keys (like clickup_task_id) are passed to tools that don't accept them. Co-Authored-By: Claude Opus 4.6 --- cheddahbot/scheduler.py | 66 +++++++++++++-------- cheddahbot/tools/__init__.py | 9 +++ cheddahbot/tools/press_release.py | 95 ++++++++++++++++++++++--------- identity/USER.md | 3 + 4 files changed, 121 insertions(+), 52 deletions(-) diff --git a/cheddahbot/scheduler.py b/cheddahbot/scheduler.py index f88c40d..4e4ce7b 100644 --- a/cheddahbot/scheduler.py +++ b/cheddahbot/scheduler.py @@ -318,14 +318,16 @@ class Scheduler: state["started_at"] = now self.db.kv_set(kv_key, json.dumps(state)) - # Set ClickUp status to "in progress" client = self._get_clickup_client() - client.update_task_status(task_id, self.config.clickup.in_progress_status) try: # Build tool arguments from field mapping args = self._build_tool_args(state) + # Pass clickup_task_id so the tool can handle its own ClickUp sync + # (status updates, comments, attachments) if it supports it. + args["clickup_task_id"] = task_id + # Execute the skill via the tool registry if hasattr(self.agent, "_tools") and self.agent._tools: result = self.agent._tools.execute(skill_name, args) @@ -335,30 +337,46 @@ class Scheduler: f"Task description: {state.get('custom_fields', {})}" ) - # Extract and upload any docx deliverables - docx_paths = _extract_docx_paths(result) - state["deliverable_paths"] = docx_paths - uploaded_count = 0 - for path in docx_paths: - if client.upload_attachment(task_id, path): - uploaded_count += 1 - else: - log.warning("Failed to upload %s for task %s", path, task_id) + # Check if the tool already handled ClickUp sync internally + tool_handled_sync = "## ClickUp Sync" in result - # Success - state["state"] = "completed" - state["completed_at"] = datetime.now(UTC).isoformat() - self.db.kv_set(kv_key, json.dumps(state)) + if tool_handled_sync: + # Tool did its own status updates, comments, and attachments. + # Just update the kv_store state. + state["state"] = "completed" + state["completed_at"] = datetime.now(UTC).isoformat() + self.db.kv_set(kv_key, json.dumps(state)) + else: + # Tool doesn't handle sync — scheduler does it (fallback path). + # Set status to "in progress" (tool didn't do it) + client.update_task_status(task_id, self.config.clickup.in_progress_status) - # Update ClickUp - client.update_task_status(task_id, self.config.clickup.review_status) - attach_note = f"\nšŸ“Ž {uploaded_count} file(s) attached." if uploaded_count else "" - comment = ( - f"āœ… CheddahBot completed this task.\n\n" - f"Skill: {skill_name}\n" - f"Result:\n{result[:3000]}{attach_note}" - ) - client.add_comment(task_id, comment) + # Extract and upload any docx deliverables + docx_paths = _extract_docx_paths(result) + state["deliverable_paths"] = docx_paths + uploaded_count = 0 + for path in docx_paths: + if client.upload_attachment(task_id, path): + uploaded_count += 1 + else: + log.warning("Failed to upload %s for task %s", path, task_id) + + # Success + state["state"] = "completed" + state["completed_at"] = datetime.now(UTC).isoformat() + self.db.kv_set(kv_key, json.dumps(state)) + + # Update ClickUp + client.update_task_status(task_id, self.config.clickup.review_status) + attach_note = ( + f"\nšŸ“Ž {uploaded_count} file(s) attached." if uploaded_count else "" + ) + comment = ( + f"āœ… CheddahBot completed this task.\n\n" + f"Skill: {skill_name}\n" + f"Result:\n{result[:3000]}{attach_note}" + ) + client.add_comment(task_id, comment) self._notify( f"ClickUp task completed: **{task_name}**\n" diff --git a/cheddahbot/tools/__init__.py b/cheddahbot/tools/__init__.py index 3a140ee..c1dd3f6 100644 --- a/cheddahbot/tools/__init__.py +++ b/cheddahbot/tools/__init__.py @@ -159,6 +159,15 @@ class ToolRegistry: "memory": self.agent._memory, "agent_registry": self.agent_registry, } + + # Filter args to only params the function accepts (plus **kwargs) + has_var_keyword = any( + p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values() + ) + if not has_var_keyword: + accepted = set(sig.parameters.keys()) + args = {k: v for k, v in args.items() if k in accepted} + result = tool_def.func(**args) return str(result) if result is not None else "Done." except Exception as e: diff --git a/cheddahbot/tools/press_release.py b/cheddahbot/tools/press_release.py index e69b33e..48ccc39 100644 --- a/cheddahbot/tools/press_release.py +++ b/cheddahbot/tools/press_release.py @@ -43,6 +43,24 @@ def _set_status(ctx: dict | None, message: str) -> None: ctx["db"].kv_set("pipeline:status", message) +def _get_clickup_client(ctx: dict | None): + """Create a ClickUpClient from tool context, or None if unavailable.""" + if not ctx or not ctx.get("config") or not ctx["config"].clickup.enabled: + return None + try: + from ..clickup import ClickUpClient + + config = ctx["config"] + return ClickUpClient( + api_token=config.clickup.api_token, + workspace_id=config.clickup.workspace_id, + task_type_field_name=config.clickup.task_type_field_name, + ) + except Exception as e: + log.warning("Could not create ClickUp client: %s", e) + return None + + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -394,6 +412,25 @@ def write_press_releases( agent = ctx["agent"] + # ── ClickUp: set "in progress" and post starting comment ──────────── + cu_client = None + if clickup_task_id: + cu_client = _get_clickup_client(ctx) + if cu_client: + try: + config = ctx["config"] + cu_client.update_task_status( + clickup_task_id, config.clickup.in_progress_status + ) + cu_client.add_comment( + clickup_task_id, + f"šŸ”„ CheddahBot starting press release creation.\n\n" + f"Topic: {topic}\nCompany: {company_name}", + ) + log.info("ClickUp task %s set to in-progress", clickup_task_id) + except Exception as e: + log.warning("ClickUp start-sync failed for %s: %s", clickup_task_id, e) + # Load skill prompts try: pr_skill = _load_skill("press_release_prompt.md") @@ -550,6 +587,25 @@ def write_press_releases( text_to_docx(clean_result, docx_path) docx_files.append(str(docx_path)) + # ── ClickUp: upload docx attachments + comment ───────────────────── + uploaded_count = 0 + if clickup_task_id and cu_client: + try: + for path in docx_files: + if cu_client.upload_attachment(clickup_task_id, path): + uploaded_count += 1 + else: + log.warning("ClickUp: failed to upload %s for task %s", path, clickup_task_id) + cu_client.add_comment( + clickup_task_id, + f"šŸ“Ž Saved {len(docx_files)} press release(s). " + f"{uploaded_count} file(s) attached.\n" + f"Generating JSON-LD schemas next...", + ) + log.info("ClickUp: uploaded %d attachments for task %s", uploaded_count, clickup_task_id) + except Exception as e: + log.warning("ClickUp attachment upload failed for %s: %s", clickup_task_id, e) + # ── Step 4: Generate 2 JSON-LD schemas (Sonnet + WebSearch) ─────────── log.info("[PR Pipeline] Step 4/4: Generating 2 JSON-LD schemas...") schema_texts: list[str] = [] @@ -629,54 +685,35 @@ def write_press_releases( output_parts.append(f"| {c['step']} | {c['model']} | {c['elapsed_s']} |") output_parts.append(f"| **Total** | | **{round(total_elapsed, 1)}** |") - # ── ClickUp sync (when triggered from chat with a task ID) ─────────── - if clickup_task_id and ctx and ctx.get("config") and ctx["config"].clickup.enabled: + # ── ClickUp: completion — status to review + final comment ────────── + if clickup_task_id and cu_client: try: - from ..clickup import ClickUpClient - config = ctx["config"] - client = ClickUpClient( - api_token=config.clickup.api_token, - workspace_id=config.clickup.workspace_id, - task_type_field_name=config.clickup.task_type_field_name, - ) - # Upload each .docx as an attachment - uploaded_count = 0 - for path in docx_files: - if client.upload_attachment(clickup_task_id, path): - uploaded_count += 1 - else: - log.warning("ClickUp: failed to upload %s for task %s", path, clickup_task_id) - - # Post a result comment + # Post completion comment attach_note = f"\nšŸ“Ž {uploaded_count} file(s) attached." if uploaded_count else "" result_text = "\n".join(output_parts)[:3000] comment = ( - f"āœ… CheddahBot completed this task (via chat).\n\n" + f"āœ… CheddahBot completed this task.\n\n" f"Skill: write_press_releases\n" f"Result:\n{result_text}{attach_note}" ) - client.add_comment(clickup_task_id, comment) + cu_client.add_comment(clickup_task_id, comment) - # Update task status to review - client.update_task_status(clickup_task_id, config.clickup.review_status) + # Set status to internal review + cu_client.update_task_status(clickup_task_id, config.clickup.review_status) # Update kv_store state if one exists db = ctx.get("db") if db: - import json as _json - kv_key = f"clickup:task:{clickup_task_id}:state" existing = db.kv_get(kv_key) if existing: - state = _json.loads(existing) + state = json.loads(existing) state["state"] = "completed" state["completed_at"] = datetime.now(UTC).isoformat() state["deliverable_paths"] = docx_files - db.kv_set(kv_key, _json.dumps(state)) - - client.close() + db.kv_set(kv_key, json.dumps(state)) output_parts.append("\n## ClickUp Sync\n") output_parts.append(f"- Task `{clickup_task_id}` updated") @@ -689,6 +726,8 @@ def write_press_releases( output_parts.append("\n## ClickUp Sync\n") output_parts.append(f"- **Sync failed:** {e}") output_parts.append("- Press release results are still valid above") + finally: + cu_client.close() return "\n".join(output_parts) diff --git a/identity/USER.md b/identity/USER.md index 527eeda..e9096f0 100644 --- a/identity/USER.md +++ b/identity/USER.md @@ -17,3 +17,6 @@ - If he says he is going to bed or going away for awhile, you can ask if we can run in unattended mode - he'll usually say yes. - Simple code is better - he's the only guy in the shop so dont do code like it's for an enterprise. Simple and maintainable is important. - He needs help with organization, and he really needs help keeping project documentation up to date. Do that for him. + +## Strict Protocols +- STRICT METADATA ADHERENCE: You must never alias, rename, truncate, or 'clean up' task titles, client names, or IDs retrieved from the ClickUp API. All references to tasks must use the verbatim strings returned by the API. Proactive 'organization' of task data that alters the original text is strictly forbidden.