#!/usr/bin/env python3 """Upsert the 'marvin' interactive persona into the turnstone DB. The deployment runs turnstone-server only (no console), so persona management happens directly on the SQLite database shared into the VM (data/turnstone.db, from the host). The persona text is versioned here so it can be re-applied after a DB reset. Usage: python3 scripts/persona.py [--db data/turnstone.db] """ from __future__ import annotations import argparse import datetime import json import sqlite3 import sys PERSONA_ID = "op-marvin" NAME = "marvin" DISPLAY_NAME = "Marvin (VM)" DESCRIPTION = ( "Coding agent tailored to this container-VM: workspace persistence " "rules, Gitea/GitHub remotes, rootless podman." ) APPLIES_TO_KINDS = ["interactive"] BASE_PROMPT = """\ You are Marvin, a software engineer working inside a disposable Arch \ Linux VM. Environment facts that shape how you work: - Your workspace is /home/marvin/work — shared with the host and the \ only place that survives a VM restart. Keep all valuable work there, \ or push it to a git remote. Anything else you create (installed \ packages, pip installs, git clones outside ~/work) is discarded when \ the VM shuts down. - Git remotes live on self-hosted Gitea at git.nubo.sh (use the `tea` \ CLI) and GitHub (use `gh`). Open pull requests instead of committing \ to main unless asked. - Rootless podman is available for containerized builds and tests. - The turnstone web UI and the database live on the host side and are \ not yours to manage. Work like an engineer: read before you edit, verify before you commit, \ diagnose before you retry. When a request is ambiguous, make a \ reasonable call and state what you assumed. When you're uncertain, say \ so. Report what you did and what you didn't — finished work should be \ reviewable without archaeology.\ """ def upsert(db_path: str) -> None: now = datetime.datetime.now().isoformat(timespec="seconds") con = sqlite3.connect(db_path, timeout=10) try: with con: con.execute( """ INSERT INTO personas ( persona_id, name, display_name, description, base_prompt, base_prompt_file, tool_allowlist, mcp_enabled, memory_enabled, applies_to_kinds, is_default, enabled, org_id, created_by, created, updated ) VALUES (?, ?, ?, ?, ?, NULL, NULL, 1, 1, ?, 1, 1, '', ?, ?, ?) ON CONFLICT(persona_id) DO UPDATE SET display_name = excluded.display_name, description = excluded.description, base_prompt = excluded.base_prompt, applies_to_kinds = excluded.applies_to_kinds, is_default = 1, enabled = 1, updated = excluded.updated """, ( PERSONA_ID, NAME, DISPLAY_NAME, DESCRIPTION, BASE_PROMPT, json.dumps(APPLIES_TO_KINDS), "marvin", now, now, ), ) # Exactly one default per kind: demote the incumbent. con.execute( """UPDATE personas SET is_default = 0, updated = ? WHERE is_default = 1 AND applies_to_kinds LIKE '%interactive%' AND persona_id != ?""", (now, PERSONA_ID), ) finally: con.close() print(f"persona '{NAME}' is_default for interactive in {db_path}") if __name__ == "__main__": ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--db", default="data/turnstone.db") args = ap.parse_args() try: upsert(args.db) except sqlite3.Error as e: sys.exit(f"persona upsert failed: {e}")