Perseus Directives Reference
June 8, 2026 · View on GitHub
Directives appear in standard .md files that begin with @perseus on the first line. They are resolved at render time. The assistant never sees directive syntax — only the resolved output.
All directives follow the pattern: @directive [args] [@cache modifier]
Shell
@query
Run a shell command and embed stdout. Shell execution can be disabled via render.allow_query_shell.
@query "docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'"
@query "git log --oneline -5" @cache session
@query "python scripts/status.py" schema="status"
Options:
@cache session— run once per session, reuse output on subsequent renders@cache ttl=300— cache for N secondsfallback="text"— output to show if command fails (non-zero exit), returns empty stdout, times out, or raises an exception. Returns the literal text as a bare string (no fence). Both"text"and'text'quoting work. Backslash escapes are honored (\n,\t, etc.). Composes with@cache: write the fallback BEFORE the cache modifier.schema="name.yaml"— parse stdout as YAML and validate it against a schema. Relative schema names resolve from.perseus/schemas/first, then the workspace root. Extensionless names try.yamland.yml.
Files
@read
Read a structured file and extract a value.
@read ./package.json path="version"
@read .env key="PORT" fallback="3001"
@read config.yaml path="database.host"
@read config.yaml path="database" schema="database"
Supported formats: JSON, YAML, TOML, .env, plaintext.
Options:
path="a.b.0"— extract a nested value from JSON/YAML/TOML.key="NAME"— extractNAME=valuefrom.env-style files.fallback="text"— value to return when the file, key, or path is missing.schema="name.yaml"— validate the full file, extracted path,.envkey, or fallback before injecting it.
@include
Inline the full contents of a file at the directive site.
@include ./CHANGELOG.md
@include ~/.perseus/waypoints/latest.yaml @cache session
@list
List directory contents or array data.
@list ./packages/ type="dirs" depth=1 as="list"
@list ./package.json path="scripts" columns="key:Command,value:Runs" as="table"
@tree
Directory tree, optionally filtered.
@tree ./src/ depth=3 match="*.py"
Environment
@env
Resolve an environment variable.
@env NODE_ENV fallback="development"
@env DATABASE_URL required=true
@env DEPLOY_ENV schema="deploy-env"
If required=true and the var is unset, the rendered output includes a ⚠ MISSING warning rather than silently omitting it.
schema="name.yaml" validates the resolved value, or the fallback when the
environment variable is unset.
Validation
@validate
Validate a rendered block before injecting it. The block body is rendered first, then parsed as YAML. If the rendered body is a single fenced code block, Perseus validates the fenced payload.
@validate schema="service"
@read config/service.yaml
@end
If validation fails, Perseus emits a visible warning instead of the invalid payload.
Time
@date
Current date/time.
@date format="YYYY-MM-DD HH:mm z"
@date format="relative" → "3 minutes ago" style
Session & Assistant State
@session
Digest of recent assistant sessions — titles, timestamps, brief summaries. Reads from the
sessions_dir path configured under assistant.sessions_dir in ~/.perseus/config.yaml.
Default: ~/.hermes/sessions (Hermes Agent). Configure for your assistant:
# ~/.perseus/config.yaml
assistant:
sessions_dir: "~/.codex/sessions" # Codex sessions
sessions_dir: "~/.claude/sessions" # Claude Code sessions
@session count=5
@session count=3 topic="ntfy OR webhook"
Output is a compact markdown list of recent sessions the assistant can use to understand active threads.
@skills
List available skills for the configured assistant, with optional staleness flags and category filters.
Reads from the skill_dir path configured under assistant.skill_dir in ~/.perseus/config.yaml.
Default: ~/.hermes/skills/ (Hermes Agent). Configure for your assistant:
# ~/.perseus/config.yaml
assistant:
skill_dir: "~/.codex/skills" # Codex skills
skill_dir: "~/.claude/skills" # Claude Code skills
skill_dir: "~/.cursor/skills" # Cursor skills
@skills
@skills category=github
@skills category=devops,media,github → comma-separated multi-category filter
@skills include=devops → alias for category=
@skills flag_stale=true → marks skills not updated in >30 days
@skills flag_stale=true category=devops,software-development
| Modifier | Description |
|---|---|
category=<name> | Filter to skills whose path starts with <name>/. Comma-separated for multiple: category=devops,media. |
include=<name> | Alias for category=. |
flag_stale=true | Append ⚠ stale marker to skills not updated within stale_skill_days (default 30). Can be combined with category=. |
@waypoint
Include the most recent checkpoint (or a specific one).
@waypoint → latest
@waypoint ttl=3600 → only if written within the last hour
Services
@services
Health-check a list of endpoints or containers. Supports either a YAML block immediately following @services or an explicit @services ... @end block. command: checks are gated by render.allow_services_command.
@services
- name: Hermes WebUI
url: http://localhost:7779/health
- name: ntfy
url: http://localhost:8080/v1/health
- name: Portainer
url: https://localhost:9443/api/status
- name: mongo-dev
docker: mongo-dev
Output: structured table with name, status (✅/❌), and latency.
Conditional
@if / @else / @endif
Malformed or unknown conditions render a visible warning block rather than silently evaluating false.
@if env.DATABASE_URL != ""
Database is configured.
@else
⚠ DATABASE_URL is not set — app will not start.
@endif
@if file.exists ".env"
@include .env
@else
No .env found.
@endif
@if query("docker ps | grep mongo-test") matches /mongo-test/
Mongo test container is running on port 27018.
@else
Port 27018 is clear.
@endif
@if query("git status --porcelain") not matches /./
Working tree is clean.
@endif
@if query("uname -a") matches /Darwin/i
Running on macOS.
@endif
Supported condition forms:
| Form | Description |
|---|---|
file.exists "path" | True if the file/dir exists |
file.missing "path" | True if the file/dir does NOT exist |
env.set VAR | True if $VAR is set and non-empty |
env.unset VAR | True if $VAR is unset or empty |
env.eq VAR "value" | True if $VAR equals the literal value |
env.neq VAR "value" | True if $VAR does not equal the literal value |
query("cmd") matches /regex/[i] | Run the command and test stdout against the regex; i = case-insensitive |
query("cmd") not matches /regex/[i] | Negated form |
@if query(...) honors render.allow_query_shell — when false, the
condition evaluates to False and a stderr warning is printed (parsing does
not raise). Invalid regex raises a ConditionParseError so the renderer
surfaces a visible warning instead of silently failing.
AI-Native
@constraint
A machine-readable rule rendered as a structured table row. Clearer signal to the assistant than buried prose.
@constraint id="no-direct-db" severity="critical"
NEVER import the database driver directly. All DB access goes through src/db/index.ts.
@end
@constraint id="api-prefix" severity="critical"
Every route MUST use /api/v1/ prefix.
@end
Rendered as:
| ID | Severity | Rule |
|---|---|---|
| no-direct-db | CRITICAL | NEVER import the database driver directly... |
| api-prefix | CRITICAL | Every route MUST use /api/v1/ prefix. |
@prompt
An instruction embedded in the source that is included in AI render mode and stripped in human render mode.
@prompt
This document was rendered live. Do not look up package.json, .env, or
docker ps — all current values are already below.
@end
Project Memory
@memory
Inject the Mnēmē narrative for the current workspace inline. Reads the
narrative file at ~/.perseus/memory/<workspace-hash>.md. If no narrative
exists yet, renders a warning advising the user to run perseus memory update.
If the narrative is stale (age > checkpoints.ttl_s), renders a staleness
warning and preserves the stale timestamp. A fresh successful narrative render
touches the frontmatter updated timestamp so callers can detect the last
access time.
@memory
@memory focus="decisions"
@memory focus="recent"
@memory ttl=3600
@memory @cache ttl=3600
Arguments:
| Arg | Values | Description |
|---|---|---|
focus | "arc", "decisions", "history" (alias "tasks"), "patterns", "recent" | Emit only the named ## section from the narrative body |
ttl | integer (seconds) | Short-form cache — equivalent to @cache ttl=N |
See spec/components.md § 4 (Mnēmē) for the CLI surface that produces the
narrative file.
@memory federation (task-19, Phase 8.2)
Render a digest of subscribed cross-workspace narratives. Subscriptions are
defined in ~/.perseus/memory/federation.yaml and managed via the
perseus memory federation CLI.
@memory federation ← all enabled subscriptions
@memory federation alias=hermes ← single subscription (even if disabled)
@memory include_federation=true ← local narrative + appended digest
Hard guarantee: plain @memory is local-only forever — federated content
never silently appears in plain @memory output even when subscriptions are
configured. Use one of the two forms above to opt in (Q3 decision).
Failure semantics (Q5):
| Condition | Behavior |
|---|---|
| Workspace path missing | Inline warning block per alias, render continues |
| Narrative file missing | Inline warning block per alias, render continues |
| Narrative YAML malformed | Inline warning block per alias, render continues |
Narrative stale (> checkpoints.ttl_s) | Body included WITH stale warning |
| Manifest file malformed | Empty digest + stderr warning, no exception |
Privacy posture (Q6): subscriber-side only. If workspace A can read workspace B's narrative file, Perseus cannot meaningfully enforce publisher-side access control on a shared filesystem. Documented; not defended.
Maintenance
@health
Inline maintenance suggestions for the current workspace. Same content as
perseus health writes to stdout. Read-only — runs deterministic heuristics
and emits a markdown list.
@health
Heuristics emitted (when applicable):
- Stale Checkpoints — checkpoints older than
health.stale_checkpoint_days - Duplicate Checkpoints — repeated (task, status, next) tuples in the last
health.duplicate_checkpoint_windowcheckpoints - Context Source Size —
.perseus/context.mdexceedshealth.context_line_warning - Old Completed Tasks — Agora tasks closed more than
health.include_completed_tasks_older_than_daysdays ago
When nothing is flagged, emits _All clear — no maintenance suggestions._
Tasks (Agora)
@agora
Live task board from tasks/ directory. Renders a markdown table grouped by
status and optionally filtered by scope.
@agora
@agora status=open
@agora status=in_progress scope=small,medium
Arguments:
| Arg | Values | Description |
|---|---|---|
status | open, in_progress, completed, all (default) | Filter by frontmatter status |
scope | comma list (e.g. small,medium) | Filter by frontmatter scope |
Task files live under cfg.agora.tasks_dir (default: tasks/) and must have
YAML frontmatter with id, title, status, scope, optional claimed_by,
opened, closed, depends_on.
See perseus agora list / claim / complete for managing tasks from the CLI.
Caching
The @cache modifier can be appended to any directive:
| Modifier | Behavior |
|---|---|
@cache session | Resolved once per render session; reused for subsequent renders |
@cache ttl=N | Cached for N seconds |
@cache persist | Written to disk cache; survives across sessions until TTL expires |
@cache mock | Returns a static mock value (for testing/offline use) |
Subprocess (Phase 8)
@agent
Run a local subprocess and embed its stdout inline.
@agent "echo hello"
@agent "my-script.sh" timeout=30 fallback="(unavailable)"
@agent "git diff --stat HEAD~1" strip=true @cache ttl=300
| Arg | Default | Description |
|---|---|---|
timeout | 10 | Seconds before subprocess is killed |
strip | true | Strip leading/trailing whitespace from stdout |
fallback | (none) | Substitute text on failure/timeout instead of warning |
Differs from @query in three ways:
- Output is substituted INLINE (no fenced code block by default)
- Failure with
fallback=silently substitutes the fallback text - Gated by
render.allow_agent_shell(defaulttrue)
Composes with @cache session|ttl=N|persist|mock.
Messaging (Phase 8)
@inbox
Render pending point-to-point messages from perseus inbox send.
@inbox
@inbox unread=true
@inbox limit=5
Dismissed messages are always excluded. unread=true filters to messages that
have not been opened via perseus inbox read.
Empty inbox renders _No new messages._
Mnēmē v2 Memory Search
@mneme
Search the persistent Mnēmē vault via BM25 over SQLite FTS5.
@mneme query="deployment pipeline"
@mneme query="error handling" scope="project" k=10
@mneme query="decisions" type="decision"
Options:
query="..."— search query (required). Wrapped as an FTS5 phrase for safety.scope="..."— filter by scope (e.g.project,team,personal).type="..."— filter by memory type (lesson,decision,preference,workflow, etc.).k=N— max results (default 5, max 100).
Oracle Drift
@drift
Generate an Oracle drift report showing discrepancies between rendered context and expectation.
@drift
Requires Pythia Oracle to be enabled. Returns structured drift data.
Foreign Resolver
@perseus
Fetch rendered context from a remote Perseus serve instance.
@perseus https://other-host:8420/workspace/my-project @cache ttl=300
Options:
- URL must be
http://orhttps://with a/workspace/<name>path. @cache ttl=N— cache the remote response for N seconds (recommended).foreign.verify_signatures=true— enable HMAC-SHA256 verification.foreign.shared_secret— pre-shared key for HMAC (min 32 chars).foreign.allow_internal=true— allow RFC1918/loopback destinations (disabled by default for SSRF protection).
External Tool
@tool
Run an allowlisted external tool and embed its output.
@tool "path/to/my-tool" arg1 arg2
@tool "validator" --check ./data.json timeout=30
Options:
- First argument is the tool name/path (must be in
tools.allowlist). timeout=N— max execution time in seconds.max_output_bytes=N— truncate output (default 65536).- Tools are gated by
executes_shell=truein the tool registry.
Synthesis Block
@synthesize
Optional curated synthesis section. Only rendered when generation.enabled=true.
@synthesize question="What are the key risks?" source="@health @agora"
@synthesize question="Summarize recent changes" label="Weekly Summary" consistency_mode=strict
Options:
question="..."— the synthesis prompt.source="..."— space-separated list of directives whose output informs the synthesis.label="..."— optional heading label.consistency_mode=strict— enforce consistency with source directives.
Requires: generation.enabled=true in config.
See spec/components.md § 8 (Inbox) for the CLI surface.