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:
1 parent
299e412518
commit
93fa05bdcc
2 files changed
+127
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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}")
|
||||
Reference in new issue
Block a user