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: Pre-release. The CLI, pack runtime, orchestrator, watch daemon, and marketplace server are implemented. The public registry API is online; the browser marketplace remains behind an access gate while release validation finishes.
Desktop app: download.frameshift.syntheos.dev -- a proprietary companion app distributed separately from this engine repository.
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 the latest signed release from the public registry:
frameshift install cryptographic
frameshift use cryptographic
# Or install a complete local pack directory:
frameshift install cryptographic@0.1.0 --from-path /path/to/cryptographic
frameshift activate cryptographicRegistry commands use https://frameshift-api.syntheos.dev by default. Set FRAMESHIFT_REGISTRY_URL to target another deployment. The personas/ directory in this repository is a manifest-only public catalog, not a complete local pack library; install those personas from the registry unless you also have their behavioral source.
Automate mode lets a host integration pick the persona for you. Frameshift classifies the task, ranks installed personas against project context, and stores the mode, sensitivity, lock, preferences, and transition audit. A session hook or other host integration decides when to run selection and activate a result; frameshift automate on does not switch personas by itself.
# 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 default 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. Hosts can consume JSON output and let the active model rerank the candidates before activation.
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 using personas installed for this project (default):
frameshift select --task "debug a rust compilation error"
# Structured JSON for programmatic consumption or LLM reranking:
frameshift select --task "debug a rust compilation error" --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. Pass --library /path/to/library only when each persona directory contains AGENTS.md or typed persona source; a manifest-only catalog has no behavioral content to rank.
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 persona body plus a typed manifest, renders the instruction file each agent expects, and distributes the result as a signed, content-addressed pack that installs into a central store outside your project tree. Structured TOML source is available when a persona needs composition or semantic editing.
A persona ships as a pack: a pack.toml manifest plus behavioral content. Marketplace packs use freeform AGENTS.md as the primary body; CLAUDE.md, GEMINI.md, or README.md are accepted as fallback bodies. Frameshift also supports an optional typed source made from persona.toml, rules.toml, skills.toml, and patterns.toml for structured editing and composition. 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). For typed packs, 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. Author keys are immutable in the current release: there is no public key-rotation endpoint. 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. The client also pins each handle's first observed registry key locally and rejects later key changes. 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 registration and publishing, 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 is reached directly.
Registry reads are public. Registration, publication, and administration require Ed25519-signed requests that bind the method, path, body hash, timestamp, and nonce. The server checks clock skew, claims nonces in the shared catalog to reject cross-instance replay, and admits publishers through FRAMESHIFT_PUBLISHER_PUBKEYS.
Request-body and archive limits bound memory and decompression work. Publication also enforces per-author version and byte quotas plus a registry-wide byte quota. Uploaded archives may contain at most 256 regular-file entries and decompress to at most 16 MiB; unsafe paths and non-regular entries are rejected before catalog registration.
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) runs a persona's conformance bundle through a runner and reports the score; it does not modify pack.toml. A published baseline must correspond to the bundle shipped in the pack.
Freeform packs use their Markdown body for every target, with Frameshift's host overlay and persona header prepended. Typed packs use four TOML files -- persona.toml (identity, voice, anchors), rules.toml, skills.toml, and patterns.toml -- and render target-specific Markdown. Typed rules carry a layer: L1 (non-negotiable invariants), L2 (contextual defaults, overridable with explicit justification), L3 (preferences).
Both paths write the file each agent expects into rendered/<target>/: CLAUDE.md for Claude, AGENTS.md for Codex and generic, and GEMINI.md for Gemini. Typed targets differ in which sections they carry: Claude and generic get the full document, Codex omits Design Notes and Safety-Layer sections, and Gemini omits Design Notes.
A typed 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 packs already installed in the same project. A freeform pack that declares composition metadata cannot be structurally composed; Frameshift warns and renders its Markdown body unchanged.
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 projectA pack can ship a pack.template.toml manifest declaring {{token}} placeholders its markdown uses -- personal values like how the agent should address you that belong to the user, not the pack:
[tokens.principal_address]
type = "string"
required = true
description = "How the agent should address you"Token values live in a per-project vault: a single age-encrypted file in the central store (never in the project root, never in the pack). When install/activate/use/sync write a persona's rendered/<target>/ outputs, every {{token}} is substituted from the vault (the separate frameshift render debug command renders typed source directly and does not substitute tokens). A missing required = true token fails the render with one error naming every missing token; an optional token without a value keeps its literal {{name}} placeholder. Packs that ship no pack.template.toml render without opening the vault.
The vault passphrase comes from FRAMESHIFT_VAULT_PASSPHRASE, or a hidden interactive prompt when the CLI runs in a terminal. Only the CLI ever prompts. The daemon and MCP server resolve the passphrase from the environment variable alone: rendering a templated pack there without it set fails with an error rather than degrading silently (packs without pack.template.toml are unaffected either way). There is no built-in passphrase recovery -- losing the passphrase means losing the vault's contents, so keep your own backup.
frameshift vault init # create this project's empty vault
frameshift vault set principal_address # prompts for the value, hidden
frameshift vault list # keys only, never valuesThe 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) to MCP-capable hosts. - 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, optionally reranks the candidates with the active model, and activates the selected persona when transition policy allows it.
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>] Activate + print output; optionally install from a local library
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 host-driven automate policy
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
Vault and project config:
frameshift vault init Create this project's vault (refuses if one exists)
frameshift vault set <key> [--value <v>] Set a token value (prompts hidden when --value is omitted;
prefer the prompt -- --value lands in shell history)
frameshift vault get <key> Print a token's raw value
frameshift vault rm <key> Remove a token
frameshift vault list List token keys (never values)
frameshift config get <key> Print a key from the project's central config.toml
frameshift config set <key> <value> Set a key (e.g. telemetry_opt_in true)
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) 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
Frameshift requires Rust 1.86 or newer. The full workspace also requires libpq (the PostgreSQL client library) for the Diesel-backed catalog crate:
# Debian/Ubuntu
sudo apt-get install libpq-dev
# macOS
brew install libpqcargo build --locked --workspace
cargo test --locked --workspaceInstall the CLI from the workspace:
cargo install --locked --path crates/frameshift-cliThe standard test command skips the Docker-backed PostgreSQL integration tests. Run those separately when Docker is available:
cargo test --locked -p frameshift-catalog-postgres -- --include-ignoredTo include the semantic-selection channel (downloads a ~23 MB embedding model on first use):
cargo build --locked -p frameshift-cli --features embeddingscargo run -p frameshift-cli -- install cryptographic
cargo run -p frameshift-cli -- use cryptographic
cargo run -p frameshift-cli -- select --task "optimize a hot loop" --format json| Variable | Default | Purpose |
|---|---|---|
FRAMESHIFT_REGISTRY_URL |
https://frameshift-api.syntheos.dev |
Registry base URL used by install, search, and telemetry endpoint derivation |
FRAMESHIFT_TELEMETRY_URL |
registry /v1/telemetry/selection route |
Optional telemetry endpoint override; telemetry still requires project opt-in |
FRAMESHIFT_VAULT_PASSPHRASE |
interactive CLI prompt | Non-interactive passphrase source for encrypted template values |
FRAMESHIFT_PROJECT_ID |
hash of canonical project path | Explicit project identity override for hosts that cannot expose a stable path |
FRAMESHIFT_ALLOW_CONFORMANCE_INTEGRITY_FAILURE |
unset | Set to 1 to explicitly bypass an upgrade's conformance-bundle integrity failure |
Most variables are read with no prefix (e.g. BIND_ADDR, not FRAMESHIFT_BIND_ADDR). Publisher and administrator admission lists deliberately use FRAMESHIFT_PUBLISHER_PUBKEYS and FRAMESHIFT_ADMIN_PUBKEYS; 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 |
ABUSE_RATE_PER_MIN |
60 |
Per-IP rate limit on signed writes and telemetry (requests/minute); 0 disables |
METRICS_BEARER_TOKEN |
empty | Bearer token required by /metrics; empty disables the endpoint |
FRAMESHIFT_PUBLISHER_PUBKEYS |
empty | Comma-separated admitted base64url-no-pad Ed25519 keys; empty disables writes, * admits any valid signer |
MAX_VERSIONS_PER_AUTHOR |
100 |
Maximum retained versions per admitted author; 0 disables |
MAX_BYTES_PER_AUTHOR |
1073741824 |
Maximum retained archive bytes per admitted author; 0 disables |
MAX_TOTAL_BYTES |
107374182400 |
Maximum retained archive bytes across the registry; 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. Commercial terms for hosted or managed offerings are available from support@syntheos.dev.
