Server-only deployment has no console admin API, so the persona is upserted directly into the SQLite DB via scripts/persona.py with the prompt text versioned in the repo (VM persistence rules, Gitea/GitHub remotes, rootless podman). Built-in personas are seeded by migration 063; this sets marvin as the interactive default.
111 lines
3.9 KiB
Python
111 lines
3.9 KiB
Python
#!/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}")
|