Add custom 'marvin' persona as interactive default

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.
This commit is contained in:
marvin committed 2026-09-28 20:06:02 +02:00
1 parent 299e412518
commit 93fa05bdcc
2 files changed
+127

No files matched your search

+17
View File
@@ -104,6 +104,23 @@ Edit `configs/turnstone/config.toml` (gitignored; see
(currently mirrored from opencode's `auth.json`).
- `[auth] jwt_secret` — required, generate once.
- `[database] path` — SQLite on the `data/` share.
- `[tools]` — `workspace_dir` surfaced to the model
(`/home/marvin/work`, mounted from `./work`).
### Personas
Built-ins (Engineer, Writer, Researcher, Scribe, Manager, Executive)
are seeded by turnstone's migration. This deployment adds a custom
default persona `marvin` (VM-specific: persistence rules, Gitea remotes,
podman) via `scripts/persona.py` — the prompt text is versioned there
and re-applied after a DB reset:
```sh
python3 scripts/persona.py # --db data/turnstone.db default
```
(Without a console deployment, persona CRUD is a direct SQLite write;
with `turnstone-console` running, use its admin API/Personas tab.)
## Notes / trade-offs
+110
View File
@@ -0,0 +1,110 @@
#!/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}")