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`).
|
(currently mirrored from opencode's `auth.json`).
|
||||||
- `[auth] jwt_secret` — required, generate once.
|
- `[auth] jwt_secret` — required, generate once.
|
||||||
- `[database] path` — SQLite on the `data/` share.
|
- `[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
|
## 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