A persona engine for AI coding agents. Install behavioral identities as versioned packs, activate them per-project, and let the engine pick the right one for the task.
Status: The CLI, pack system, orchestrator, watch daemon, marketplace server, and web frontend all work. You can clone this repo and use the personas today via the CLI, or publish and browse packs against the live marketplace.
Desktop app: download.frameshift.syntheos.dev -- a proprietary companion app for browsing the marketplace and managing personas with one click. This repo is the engine it drives; the app ships separately with Ed25519-signed self-updates.
Personas are not instruction lists. They are complete behavioral frames -- identity, rules, skills, operating posture -- that survive long sessions, surprising inputs, and the slow drift that turns careful agents into sloppy ones around turn 200. Same model, different frame.
# Install + activate + render in one shot:
frameshift use cryptographic --from ./personas
# Or step by step:
frameshift install cryptographic@0.1.0 --from-path ./personas/cryptographic
frameshift activate cryptographicAutomate mode picks the persona for you. The engine classifies your task, scores every installed persona against the project context, and switches when the domain shifts.
# Turn on for this project:
frameshift automate on
# With a sensitivity dial (0.0 = stable, 1.0 = responsive):
frameshift automate on --sensitivity 0.7
# Check current state:
frameshift automate statusThe selection pipeline scores four components: language overlap (how well the persona's language set matches your project), lexical match (IDF-weighted task token hits against persona keywords), intent alignment (10-category task classification), and capability fit. Scores blend into a ranked list with confidence values.
The engine classifies task descriptions into one of ten intents: Implementation, Debugging, Review, Security, Writing, Ops, Testing, Refactoring, Performance, and Design. Personas declare which intents they handle best. A persona built for debugging scores higher when the task looks like debugging.
# Table format (default):
frameshift select --task "debug a rust compilation error" --library ~/.local/share/frameshift/personas
# Structured JSON for programmatic consumption or LLM reranking:
frameshift select --task "debug a rust compilation error" --library ~/.local/share/frameshift/personas --format jsonJSON output includes the full context snapshot (detected languages, frameworks, inferred intent), per-candidate component scores, matched tokens, and rationale. Host LLMs can rerank using this data.
When the engine picks wrong, record the override. The engine adjusts per-persona bias for future selections, with optional intent context and time decay.
frameshift feedback --auto-pick web-designer --chosen rust --intent debuggingFrameshift takes a typed persona definition, compiles it into the instruction file each agent expects, and distributes it as a signed, content-addressed pack that installs into a central store outside your project tree.
A persona ships as a pack: a pack.toml manifest plus the persona's behavioral content. The manifest carries identity and contract metadata:
schema_version = 1
name = "cryptographic"
version = "0.1.0"
author_handle = "ghost-frame"
author_pubkey = "1a2b3c..." # 64 hex chars (Ed25519 verifying key)
license = "Elastic-2.0"
description = "Specification-anchored cryptographic implementation"
tags = ["security", "rust"]
[capability_manifest]
required_tools = ["Read", "Edit", "Write", "Bash"]
network_egress = false
filesystem_scope = "project-only"
memory_required = "none"description and tags feed registry search; capability_manifest declares the tools, network egress, filesystem scope, and memory requirement the persona expects (see Capabilities and Memory below). extends and mixin drive composition. parent_hash tracks version lineage; conformance_baseline feeds the install-time regression gate (see Conformance below).
Every pack has a canonical hash: a SHA-256 computed from its files by normalizing each relative path (NFC, forward slashes), sorting byte-lexicographically, and hashing path \0 length \0 content \0 for each. Because it is derived from the directory's logical contents, the canonical hash is independent of how the pack is later archived or compressed -- two byte-identical pack directories always produce the same hash. This is the pack's identity and the exact value an author signs.
Signing is Ed25519 over that 32-byte hash; the signature travels alongside the pack, never inside the tarball. On the wire the registry addresses the compressed .tar.gz by a second hash (the SHA-256 of the archive bytes), which the client checks on download before it extracts anything.
There is no central signing authority. An author claims a handle (e.g. ghost-frame) with a signed request, and the registry binds that handle to the key that signed it, first-claim-wins. Key rotation must be signed by the current key -- the old key authorizes its own replacement. At publish, the server checks that the live signer owns the handle and that the pack signature verifies against that registered key. On install from the registry, the client verifies the pack signature against the key in the registry's record for that version, not the key embedded in the manifest, so a tampered manifest cannot smuggle in a different key. Installing directly from a local path verifies a signature if one is present, and installs unsigned local packs as-is. Registered authors are publicly listable via GET /v1/authors.
frameshift install fetches pack bytes from the direct, unauthenticated GET /v1/packs/{name}/versions/{version}/pack route -- this is the supported download path today. The server also implements a signed-download flow: POST .../download-url mints a short-lived, HMAC-signed /dl/{hash} URL (gated on the DOWNLOAD_SECRET env var, disabled when it is unset). That flow is fully built and tested server-side but has no caller yet in the CLI or client, so treat it as experimental / not-yet-default until something mints and follows those URLs.
The registry server exposes one operator endpoint: POST /v1/admin/packs/{name}/{version}/tombstone, which marks a published version as removed from public availability. Like the handle-claim and rotation calls, it requires a signed request; the signer's key must also appear on FRAMESHIFT_ADMIN_PUBKEYS, a separate comma-separated allowlist of Ed25519 public keys. An empty allowlist disables the endpoint outright (404, indistinguishable from an unmapped route); a signed request from a key not on the list gets 403. No CLI or client command calls this endpoint today -- it's reached directly.
frameshift install resolves a pack (from the registry, or --from-path), verifies it, and materializes it into a central store -- your project tree is never written to. The project is keyed by project-id = sha256(realpath(project_root)), so the same directory always maps to the same state regardless of how you path to it.
All state lives under $XDG_DATA_HOME/frameshift/:
cache/<canonical-hash>/ Content-addressed pack cache (shared across projects)
identity/ed25519-signing-key.bin Your managed author signing key (mode 0600)
projects/<project-id>/
config.toml Declared dependencies, telemetry opt-in, memory adapter
lock.toml Exact versions, hashes, author pubkeys
active Name of the currently active persona
automate.json automate-prefs.json Automate mode + learned selection biases
automate-audit.jsonl automate-lock.json Switch audit log + lock marker
selection-history.jsonl Local record of past selections
personas/<name>/
source/ The pack's own files
rendered/{claude,codex,gemini,generic}/ Per-agent rendered output
growth.md growth.jsonl Append-only growth log: markdown + JSONL
The lockfile records each installed persona as name, version, author handle, author pubkey, and canonical hash. Re-running install/sync rebuilds the per-project personas/ tree from the content-addressed cache to match the lockfile.
A pack's pack.toml can ship a [conformance_baseline] -- a score from 0.0 to 1.0, plus the hash of the conformance bundle it was measured against. When install would overwrite an already-installed version of the same persona in the current project, the client compares the incoming pack's baseline against the installed one, without re-running any tests:
- Pass -- the incoming score meets or exceeds the installed score.
- Regression -- the incoming score is lower. Warn-only; the install proceeds.
- MissingBaseline -- either side ships no baseline. Non-fatal; baselines are optional.
- InvalidScore -- a score is non-finite or outside 0.0-1.0. Warn-only.
- IntegrityFailure -- the incoming pack's declared
bundle_hashdoesn't match the hash of its own shipped conformance bundle, or it ships none at all. This hard-blocks the install -- an unverifiable score can't be trusted regardless of what it claims. Override withFRAMESHIFT_ALLOW_CONFORMANCE_INTEGRITY_FAILURE=1.
A fresh install of a persona with no prior version in the project skips the comparison entirely. frameshift verify (see CLI, below) is what produces a baseline in the first place: it runs a persona's conformance bundle through a runner and scores the results.
A persona's typed source is four TOML files -- persona.toml (identity, voice, anchors), rules.toml, skills.toml, and patterns.toml. Rules carry a layer: L1 (non-negotiable invariants), L2 (contextual defaults, overridable with explicit justification), L3 (preferences).
Rendering projects that source into Markdown, once per agent target, writing the file each agent expects into rendered/<target>/: CLAUDE.md for Claude, AGENTS.md for Codex and generic, GEMINI.md for Gemini. The targets differ in which sections they carry -- Claude and generic get the full document, Codex omits the Design Notes and Safety-Layer sections, Gemini omits Design Notes -- so the same source produces the idiom each agent reads best.
A pack can extends a single base persona and mixin a list of others. Composition merges in a fixed order -- base, then each mixin in turn, then the persona itself -- with later layers overriding earlier ones by rule or skill id (last write wins). One invariant is protected: a mixin can never override a base's L1 rule, and a persona can override an inherited L1 rule only by explicitly opting in. A missing base or an illegal L1 override fails the install. Bases and mixins are resolved from the packs already installed in the same project.
Each pack's capability_manifest declares the tools it expects, whether it needs network egress, its filesystem scope, and its memory requirement. Over MCP, Frameshift surfaces this contract as advisory: the frameshift_capabilities tool annotates a proposed tool list against the active persona's declared tools. It never blocks or hides the host agent's own tools -- the manifest names agent-side tools (Read, Bash, ...), a separate namespace from Frameshift's own MCP tools -- it only reports the contract.
A persona can declare a memory requirement in its manifest, and it is enforced at activation: a persona with memory_required = "hard" refuses to activate unless the project declares a memory adapter (a [memory] table in the project's central config.toml), and a "soft" requirement activates with a warning. Frameshift defines a pluggable MemoryAdapter (store, search, recall, list, forget, health) with backends for HTTP APIs and local SQLite full-text search. Any knowledge system exposing those operations works; Kleos is the reference integration. The registry server can be configured with a backend via MEMORY_BACKEND and reports its status at /v1/memory/health.
Each persona keeps an append-only local growth log -- things learned, mistakes caught, patterns discovered over a working session. Entries are dual-written to the legacy personas/<name>/growth.md and a structured growth.jsonl; grow log and grow summary read the structured form back.
frameshift grow append --persona rust --text "orphan rules prevent implementing foreign traits on foreign types"
frameshift grow log --persona rust --limit 5
frameshift grow summary --persona rust --scope projectThe same selection engine backs every surface:
- CLI --
frameshift <command>(see below). - Stdio MCP server -- a JSON-RPC server exposing tools (install, activate, list, select, use, automate, prefs, grow, capabilities, search) and prompts (
active_persona,select_persona,automate_status) as slash commands in any MCP-capable agent. - Watch daemon -- an optional background service over a peer-authenticated Unix socket, offering install/activate/sync/gc operations to editor integrations.
- Registry / marketplace HTTP server -- publish, search, download, and author/handle registration.
Automate mode itself is applied by the host integration: a session hook (or equivalent) reads the per-project automate flag, calls frameshift select for the current task, and activates the best-fit persona.
Selection blends language, lexical, intent, capability, and context signals. Built with the optional embeddings cargo feature, it adds a semantic channel: a local sentence-embedding model (all-MiniLM-L6-v2 on pure-Rust candle, ~23 MB, downloaded on first use and cached) scores the task description against each persona's description and keywords by cosine similarity. The bonus is additive and capped -- it can lift a meaning-matching persona, never penalize one -- and everything degrades to the lexical channels when the feature is off or the model is unavailable. Default builds ship none of the ML stack.
Persona lifecycle:
frameshift install <name>[@<version>] [--from-path <dir>] Install a pack (a bare name resolves the latest registry version)
frameshift uninstall <persona> Remove a persona from this project (cache is kept for gc)
frameshift activate <name> Set the active persona for this project
frameshift use <name> --from <library> Install + activate + print rendered output
frameshift list List installed personas and mark the active one
frameshift sync Reconcile the central store with the lockfile
frameshift gc Remove unreferenced cache entries
frameshift migrate Move legacy files into the central store (also migrates growth logs to JSONL)
Selection and automate mode:
frameshift select [--task TEXT] [--library DIR] [--format table|json] Rank personas by score/confidence/rationale
frameshift automate on [--sensitivity 0.0-1.0] Enable automatic persona switching
frameshift automate off | status | lock | unlock Disable / inspect / pin / unpin
frameshift feedback --chosen <name> [--auto-pick <name>] Record a selection override
[--intent <intent>] [--reason <text>]
frameshift prefs show View current per-persona bias values
frameshift prefs bump <persona> Increase a persona's bias
frameshift prefs decay <persona> Decrease a persona's bias
frameshift prefs reset Clear all recorded preferences
Authoring and registry:
frameshift rule add <persona> --id <id> --layer <L1|L2|L3> --text <text> Add a rule to a persona
frameshift rule remove <persona> --id <id> Remove a rule
frameshift skill add <persona> --id <id> --text <when> Add a skill entry to a persona
frameshift skill remove <persona> --id <id> Remove a skill entry
frameshift grow append --persona <name> --text <text> Append to a persona's growth log
frameshift grow log --persona <name> [--limit <n>] Show recent structured growth entries (default limit: 10)
frameshift grow summary --persona <name> [--scope project|global] Summarize growth entries (default scope: project)
frameshift diff <a> <b> Semantic diff between two personas
frameshift render <persona> Render persona source to markdown
frameshift verify (--persona <name> | --bundle <dir>) Run conformance checks (exactly one of the two)
[--runner mock|cli] [--model <name>] [--threshold <0.0-1.0>]
frameshift register --server <url> --handle <handle> [--display-name <name>] Claim an author handle
frameshift publish --persona <name> [--out <dir>] Build a persona pack (add --server + --handle to sign and upload)
[--server <url> --handle <handle>]
frameshift search [QUERY] [--tag <tag>] [--limit <n>] Search the registry
frameshift project-id Print the hashed project ID
verify defaults to --runner mock (canned, offline responses, used by CI) with --threshold 0.5; pass --runner cli to drive the subscription-backed agy runner against --model (default Gemini 3.1 Pro (High)), which needs a logged-in agy. publish writes the pack to --out (default publish-output/<persona>) unconditionally; the upload step only runs when --server is set, and --handle is then required.
crates/-- Rust workspace: CLI, client engine, pack tooling, composition, conformance, catalog, memory, vault, object storage, HTTP server, MCP server, watch daemon, orchestrator, embeddings, growthpersonas/-- pack manifests for the persona library
Requires libpq (the PostgreSQL client library) for the diesel/pq-sys-backed catalog crate -- install it before building the workspace:
# Debian/Ubuntu
sudo apt-get install libpq-dev
# macOS
brew install libpqcargo build
cargo testTo include the semantic-selection channel (downloads a ~23 MB embedding model on first use):
cargo build -p frameshift-cli --features embeddingscargo run -p frameshift-cli -- use cryptographic --from ./personas
cargo run -p frameshift-cli -- select --task "optimize a hot loop" --format jsonAll variables are read with no prefix (e.g. BIND_ADDR, not FRAMESHIFT_BIND_ADDR) -- except FRAMESHIFT_ADMIN_PUBKEYS, deliberately prefixed so it can't be confused with an unrelated ADMIN_PUBKEYS some other tool in the deployment might own; see crates/frameshift-server/src/config.rs for the authoritative parser.
| Variable | Default | Purpose |
|---|---|---|
BIND_ADDR |
0.0.0.0:3000 |
HTTP bind address |
POSTGRES_URL |
"" |
PostgreSQL connection URL (production must override) |
OBJECT_STORE_ROOT |
/tmp/frameshift-objects |
Filesystem object store root |
LOG_LEVEL |
info |
tracing subscriber filter string |
LOG_FORMAT |
text |
text or json |
MAX_REQUEST_BYTES |
1048576 |
Max request body size |
MAX_SEARCH_LIMIT |
200 |
Max search limit |
SHUTDOWN_GRACE |
30 |
Grace period in seconds |
CORS_ALLOWED_ORIGINS |
"" |
Comma-separated allowed CORS origins; empty disables CORS |
DOWNLOAD_SECRET |
"" |
64-char hex (32 bytes) HMAC key for signed download URLs; empty disables the signed-download endpoints |
DOWNLOAD_TOKEN_TTL |
300 |
Default TTL (seconds) for newly minted download tokens |
DOWNLOAD_MAX_TOKEN_TTL |
1800 |
Hard cap (seconds) on token TTL accepted by the verifier |
DOWNLOAD_RATE_PER_MIN |
10 |
Per-IP rate limit on the mint endpoint (requests/minute); 0 disables |
OBJECT_STORE_BACKEND |
fs |
fs (filesystem) or r2 (S3-compatible / Cloudflare R2) |
R2_ENDPOINT |
"" |
S3 endpoint URL for R2 (required when backend is r2) |
R2_BUCKET |
"" |
Bucket name (required when backend is r2) |
R2_PREFIX |
objects |
Key prefix for pack blobs inside the bucket |
R2_REGION |
auto |
S3 region (R2 always uses auto) |
R2_ACCESS_KEY_ID |
"" |
Access key ID for the bucket |
R2_SECRET_ACCESS_KEY |
"" |
Secret access key |
TRUST_FORWARDED_FOR |
false |
Trust X-Forwarded-For for rate-limit key extraction; set true only behind a trusted proxy |
SIGNED_REQUEST_MAX_SKEW_SECS |
300 |
Max clock skew (seconds) allowed between a signed write request's timestamp and server time |
FRAMESHIFT_ADMIN_PUBKEYS |
"" |
Comma-separated base64url-no-pad Ed25519 public keys allowed to call /v1/admin/* endpoints; empty disables all admin endpoints (404) |
MEMORY_BACKEND |
none |
none, http, or sqlite |
MEMORY_HTTP_ENDPOINT |
"" |
Base URL for the HTTP memory endpoint; used when MEMORY_BACKEND=http |
MEMORY_HTTP_AUTH |
none |
none or bearer:<token>; used when MEMORY_BACKEND=http |
MEMORY_HTTP_TIMEOUT_SECS |
30 |
Per-attempt request timeout for the HTTP memory adapter |
MEMORY_SQLITE_PATH |
"" |
Path to the SQLite database file; required when MEMORY_BACKEND=sqlite |
Elastic License 2.0. See LICENSE for details.
The Elastic License 2.0 prohibits offering Frameshift to third parties as a hosted or managed service. To sell, host, or distribute Frameshift on your own platform, contact us for a commercial license: support@syntheos.dev.
