diff --git a/.letta/settings.json b/.letta/settings.json index aba5448..639c5b7 100644 --- a/.letta/settings.json +++ b/.letta/settings.json @@ -6,10 +6,6 @@ { "type": "command", "command": "uv run python hooks/publish-response.py" - }, - { - "type": "command", - "command": "uv run python hooks/read-comms-reports.py" } ] }, @@ -61,6 +57,26 @@ } ] } + ], + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "uv run python hooks/central-active.py" + } + ] + } + ], + "SessionEnd": [ + { + "hooks": [ + { + "type": "command", + "command": "uv run python hooks/central-active.py" + } + ] + } ] }, "permissions": { @@ -74,7 +90,8 @@ "Bash(grep:*)", "Edit(//home/cameron/.letta/agents/agent-c770d1c8-510e-4414-be36-c9ebd95a7758/memory/system/**)", "Bash(cd /home/cameron/central && uv run python -m tools.moltbook read 057358d0 2>&1)", - "Write(**)" + "Write(**)", + "Bash(bun run tsc:*)" ] } } \ No newline at end of file diff --git a/handlers/src/notification-handler.ts b/handlers/src/notification-handler.ts index aa9b581..6b0d57c 100644 --- a/handlers/src/notification-handler.ts +++ b/handlers/src/notification-handler.ts @@ -204,6 +204,16 @@ Your response here (under 280 chars, compressed voice) - Skip notifications that don't warrant a response - READ THE THREAD CONTEXT - each notification includes thread_context showing the full conversation history. Use this to understand what you're responding to! +**Priority Assessment (OVERRIDE if needed):** +You may upgrade or downgrade the suggested priority based on your assessment: +- CRITICAL: Requires immediate response (urgent, time-sensitive, direct request from Cameron) +- HIGH: Thoughtful engagement needed (technical questions, substantive discussions) +- MEDIUM: Standard reply (general acknowledgment, routine interaction) +- LOW: Can wait (simple thanks, low-stakes engagement) +- SKIP: Don't respond (spam, off-topic, or better left alone) + +Route based on YOUR assessed priority, not the suggested one. Trust your judgment. + **Notifications to process:** ${JSON.stringify(notificationList, null, 2)} diff --git a/handlers/src/publisher.ts b/handlers/src/publisher.ts index 5346883..1c63c1e 100644 --- a/handlers/src/publisher.ts +++ b/handlers/src/publisher.ts @@ -217,17 +217,35 @@ function archiveDraft(draft: Draft): void { console.log(`Archived: ${draft.path} → ${archivePath}`); } +/** + * Check if Central is currently active (has an active Letta Code session) + */ +function isCentralActive(): boolean { + // Use import.meta.url for ES modules + const currentDir = path.dirname(new URL(import.meta.url).pathname); + const activeFile = path.join(currentDir, "..", "..", ".central-active"); + return fs.existsSync(activeFile); +} + /** * Main publisher function */ async function publishDrafts(options: { all?: boolean; auto?: boolean }) { console.log(`[${new Date().toISOString()}] Starting publisher...`); + // Check if Central is active - if so, auto-approve CRITICAL/HIGH + const centralActive = isCentralActive(); + if (centralActive) { + console.log(`[Central Active] Auto-approving CRITICAL/HIGH items`); + } + // Find drafts const draftPatterns = []; - if (options.all) { - // Include review folder (CRITICAL/HIGH) + if (options.all || centralActive) { + // Include review folder (CRITICAL/HIGH) when: + // - Explicitly requested with --all + // - Central is active (can auto-approve) draftPatterns.push(`${REVIEW_DRAFTS}/*.txt`); } diff --git a/hooks/central-active.py b/hooks/central-active.py new file mode 100644 index 0000000..405123b --- /dev/null +++ b/hooks/central-active.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +""" +Central Active Hook - SessionStart/SessionEnd hook that tracks when Central is active. + +When Central is active, the publisher can auto-approve CRITICAL/HIGH items. +When Central is inactive, CRITICAL/HIGH items stay in review queue. + +This hook handles both SessionStart and SessionEnd events: +- SessionStart: Creates .central-active marker file +- SessionEnd: Removes .central-active marker file +""" + +import sys +import json +from pathlib import Path + +# Marker file location +CENTRAL_DIR = Path(__file__).parent.parent +ACTIVE_FILE = CENTRAL_DIR / ".central-active" + + +def main(): + """Process hook input.""" + try: + input_data = json.load(sys.stdin) + except: + sys.exit(0) + + event_type = input_data.get("event_type", "") + + if event_type == "SessionStart": + # Mark Central as active + ACTIVE_FILE.write_text("active") + print(f"Central session started - auto-approval enabled", file=sys.stderr) + sys.exit(0) + + elif event_type == "SessionEnd": + # Mark Central as inactive + if ACTIVE_FILE.exists(): + ACTIVE_FILE.unlink() + print(f"Central session ended - auto-approval disabled", file=sys.stderr) + sys.exit(0) + + # Unknown event type + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/hooks/inject-comms-reports.py b/hooks/inject-comms-reports.py index 98c7668..9ce4f83 100755 --- a/hooks/inject-comms-reports.py +++ b/hooks/inject-comms-reports.py @@ -1,28 +1,20 @@ #!/usr/bin/env python3 """ -Inject Comms Reports Hook +Inject Comms Reports Hook (PostToolUse) -PostToolUse hook that captures comms subagent reports and writes them -to a well-known location for Central to consume. - -When comms finishes a Task, this hook: -1. Extracts the report from the Task result -2. Writes it to drafts/comms-reports/latest.md -3. Appends to a rolling log - -Central can then read this file to see what comms observed. +After a Task completes, check if it was comms and inject the report +back into Central's context via hookSpecificOutput.additionalContext. """ import sys import json -import os from datetime import datetime, timezone from pathlib import Path # Comms agent ID COMMS_AGENT_ID = "agent-a856f614-7654-44ba-a35f-c817d477dded" -# Output directory +# Output directory for logs SCRIPT_DIR = Path(__file__).parent.parent REPORTS_DIR = SCRIPT_DIR / "drafts" / "comms-reports" LATEST_FILE = REPORTS_DIR / "latest.md" @@ -36,49 +28,54 @@ def main(): except: sys.exit(0) - event_type = input_data.get("event_type") + event_type = input_data.get("event_type", "") tool_name = input_data.get("tool_name", "") - tool_input = input_data.get("tool_input", {}) - tool_result = input_data.get("tool_result", {}) # Only process PostToolUse for Task if event_type != "PostToolUse" or tool_name != "Task": sys.exit(0) - # Check if this was a comms task + # Get tool input and result + tool_input = input_data.get("tool_input", {}) + tool_result = input_data.get("tool_result", {}) + + # Check if this was comms by agent_id or subagent_type agent_id = tool_input.get("agent_id", "") subagent_type = tool_input.get("subagent_type", "") - # Match comms by agent_id or subagent_type is_comms = (agent_id == COMMS_AGENT_ID or subagent_type == "comms") - if not is_comms: sys.exit(0) - # Extract the report from tool result - result_message = tool_result.get("output", "") or tool_result.get("message", "") - if not result_message: + # Get the result output + if isinstance(tool_result, dict): + result = tool_result.get("output", "") or str(tool_result) + else: + result = str(tool_result) if tool_result else "" + + description = tool_input.get("description", "comms task") + + if not result or len(result) < 20: sys.exit(0) # Ensure output directory exists REPORTS_DIR.mkdir(parents=True, exist_ok=True) - # Write latest report + # Write latest report to file for reference timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - description = tool_input.get("description", "comms task") latest_content = f"""# Comms Report **Time:** {timestamp} **Task:** {description} ## Report -{result_message} +{result} """ LATEST_FILE.write_text(latest_content) # Append to rolling log - log_entry = f"\n---\n## {timestamp} - {description}\n{result_message[:1000]}\n" + log_entry = f"\n---\n## {timestamp} - {description}\n{result[:1000]}\n" with open(LOG_FILE, "a") as f: f.write(log_entry) @@ -87,7 +84,15 @@ def main(): content = LOG_FILE.read_text() LOG_FILE.write_text(content[-40000:]) - print(f"Captured comms report: {description}", file=sys.stderr) + # OUTPUT: Inject via hookSpecificOutput.additionalContext + output = { + "hookSpecificOutput": { + "hookEventName": "PostToolUse", + "additionalContext": f"📋 **Comms Report** ({description}):\n\n{result[:2000]}" + } + } + + print(json.dumps(output)) sys.exit(0)