diff --git a/src/timmy/memory_system.py b/src/timmy/memory_system.py index 79275e1..566dff4 100644 --- a/src/timmy/memory_system.py +++ b/src/timmy/memory_system.py @@ -1403,6 +1403,83 @@ def memory_forget(query: str) -> str: return f"Failed to forget: {exc}" +# ─────────────────────────────────────────────────────────────────────────────── +# Artifact Tools — "hands" for producing artifacts during conversation +# ─────────────────────────────────────────────────────────────────────────────── + +NOTES_DIR = Path.home() / ".timmy" / "notes" +DECISION_LOG = Path.home() / ".timmy" / "decisions.md" + + +def jot_note(title: str, body: str) -> str: + """Write a markdown note to Timmy's workspace (~/.timmy/notes/). + + Use this tool to capture ideas, drafts, summaries, or any artifact that + should persist beyond the conversation. Each note is saved as a + timestamped markdown file. + + Args: + title: Short descriptive title (used as filename slug). + body: Markdown content of the note. + + Returns: + Confirmation with the file path of the saved note. + """ + if not title or not title.strip(): + return "Cannot jot — title is empty." + if not body or not body.strip(): + return "Cannot jot — body is empty." + + NOTES_DIR.mkdir(parents=True, exist_ok=True) + + slug = re.sub(r"[^a-z0-9]+", "-", title.strip().lower()).strip("-")[:60] + timestamp = datetime.now(UTC).strftime("%Y%m%d-%H%M%S") + filename = f"{timestamp}_{slug}.md" + filepath = NOTES_DIR / filename + + content = f"# {title.strip()}\n\n> Created: {datetime.now(UTC).isoformat()}\n\n{body.strip()}\n" + filepath.write_text(content) + logger.info("jot_note: wrote %s", filepath) + return f"Note saved: {filepath}" + + +def log_decision(decision: str, rationale: str = "") -> str: + """Append an architectural or design decision to the running decision log. + + Use this tool when a significant decision is made during conversation — + technology choices, design trade-offs, scope changes, etc. + + Args: + decision: One-line summary of the decision. + rationale: Why this decision was made (optional but encouraged). + + Returns: + Confirmation that the decision was logged. + """ + if not decision or not decision.strip(): + return "Cannot log — decision is empty." + + DECISION_LOG.parent.mkdir(parents=True, exist_ok=True) + + # Create file with header if it doesn't exist + if not DECISION_LOG.exists(): + DECISION_LOG.write_text( + "# Decision Log\n\nRunning log of architectural and design decisions.\n\n" + ) + + stamp = datetime.now(UTC).strftime("%Y-%m-%d %H:%M UTC") + entry = f"## {stamp} — {decision.strip()}\n\n" + if rationale and rationale.strip(): + entry += f"{rationale.strip()}\n\n" + entry += "---\n\n" + + with open(DECISION_LOG, "a") as f: + f.write(entry) + + logger.info("log_decision: %s", decision.strip()[:80]) + return f"Decision logged: {decision.strip()}" + + # ─────────────────────────────────────────────────────────────────────────────── # Memory System (Central Coordinator) # ─────────────────────────────────────────────────────────────────────────────── diff --git a/src/timmy/tool_safety.py b/src/timmy/tool_safety.py index 8797796..f3e73e8 100644 --- a/src/timmy/tool_safety.py +++ b/src/timmy/tool_safety.py @@ -48,6 +48,9 @@ SAFE_TOOLS = frozenset( "check_ollama_health", "get_memory_status", "list_swarm_agents", + # Artifact tools + "jot_note", + "log_decision", # MCP Gitea tools "issue_write", "issue_read", diff --git a/src/timmy/tools.py b/src/timmy/tools.py index 8f33686..c064cf8 100644 --- a/src/timmy/tools.py +++ b/src/timmy/tools.py @@ -619,6 +619,18 @@ def _register_gematria_tool(toolkit: Toolkit) -> None: logger.debug("Gematria tool not available") +def _register_artifact_tools(toolkit: Toolkit) -> None: + """Register artifact tools — notes and decision logging.""" + try: + from timmy.memory_system import jot_note, log_decision + + toolkit.register(jot_note, name="jot_note") + toolkit.register(log_decision, name="log_decision") + except (ImportError, AttributeError) as exc: + logger.warning("Tool execution failed (Artifact tools registration): %s", exc) + logger.debug("Artifact tools not available") + + def _register_thinking_tools(toolkit: Toolkit) -> None: """Register thinking/introspection tools for self-reflection.""" try: @@ -657,6 +669,7 @@ def create_full_toolkit(base_dir: str | Path | None = None): _register_introspection_tools(toolkit) _register_delegation_tools(toolkit) _register_gematria_tool(toolkit) + _register_artifact_tools(toolkit) _register_thinking_tools(toolkit) # Gitea issue management is now provided by the gitea-mcp server