| name | compose-persona |
| description | Design-time meta-skill (v1). Given a persona brief (YAML) or a free-text
idea, produce a complete Persona artefact set: a SKILL.md
(frontmatter + executable decision_policy + prose body), a
Python registry entry pasteable into `api/shared/personas.py`, and a
graduate.sh script that mechanically wires the persona into the live
trees.
Sandbox-only; never touches real trees directly. Calls `author-persona`
as its sole sub-skill.
Sister to `compose-domain`. Where `compose-domain` graduates a whole
Durable workflow + skills + personae for one new business process,
`compose-persona` graduates ONE persona — useful when an existing
domain needs an additional approver / reviewer, or when a new
function (finance, legal, IT) needs its cast filling out before its
domain lands.
|
| audience | design-time-only |
| forbidden-runtime | true |
compose-persona (v1)
You are the orchestrator for a five-step procedure that turns a persona
brief into a complete sandboxed persona artefact set. You do not
improvise. You follow this skill literally. When you are uncertain,
you stop and ask the operator — you do not invent.
Forbidden. This skill, and any sub-skill it invokes, MUST NOT
write to any path outside tools/scratch/compose-persona/<run-id>/
and docs/superpowers/specs/. In particular: never edit
api/server/personae/, api/shared/personas.py,
api/server/services/persona_responder.py, or any other live tree.
Graduation is a separate, manual step (the operator runs the
generated graduate.sh).
Inputs
You are invoked one of two ways:
- With a brief path. "Run compose-persona against
docs/superpowers/specs/<role>-persona-brief.yaml." Skip step 1.
- With a free-text idea. "Compose a new persona for X." Run step 1
to produce the brief, then continue.
The five steps
Step 1 — Brief intake (only if no YAML brief was supplied)
Hand off to the existing brainstorming skill. It will elicit the
brief through one-question-at-a-time dialogue with the operator.
Capture the operator's answers into the YAML schema below and write
to docs/superpowers/specs/<role>-persona-brief.yaml. Read it back
to the operator. Wait for explicit approval. Then continue to step 2.
If the operator gives you a YAML brief path on entry, skip this step.
Brief schema (authoritative, v1)
persona:
role: <snake_case role name, used as folder/file prefix>
archetype: approver | subject | reviewer | delegate | notifier
scope_function: finance | hr | it | procurement | legal | legal_privacy | commercial | candidate
scope_business_unit: <string or "*">
scope_geography: <string or "*">
workflow_label: <human label of the domain this persona belongs to>
external_event:
Validate the brief before continuing:
role matches ^[a-z][a-z0-9_]*$.
archetype is one of the five allowed strings.
scope_function is one of the eight allowed strings.
external_event is ^[a-z][a-z0-9_]*$ if provided.
- If
uses_authority_mcp: true, decision_authority_action is set
AND the decision_code contains authority_check(...).
- The decision_code uses only the safe builtins from
api/server/services/persona_responder.py _DECISION_BUILTINS
(no import, no open, no os, no subprocess, no eval).
- The decision_code assigns both
decision and reason.
If validation fails, stop and tell the operator what's wrong. Do not
proceed to step 2.
Step 2 — Plan
From the brief, produce the artefact list. Print this list back to
the operator before you write a single file. Wait for "go".
Always:
- 1 persona SKILL.md at
<RUN_ROOT>/api/server/personae/<role>/SKILL.md.
- 1
REGISTRY-ENTRY.py at the run root containing the Python snippet
to splice into api/shared/personas.py PERSONAS dict.
- 1
GRADUATION.md describing what graduate.sh will do.
- 1 executable
graduate.sh that mechanically performs the live-tree
edits.
Step 3 — Inventory and isomorphism
Before invoking author-persona, read exactly these 3 files
end-to-end. They are the canonical examples this skill mirrors.
| # | Canonical file | Why |
|---|
| 1 | api/server/personae/finance_bp/SKILL.md | Authority-MCP-using persona (template for uses_authority_mcp: true) |
| 2 | api/server/personae/line_manager/SKILL.md | Inline-logic persona (template for uses_authority_mcp: false) |
| 3 | api/server/services/persona_responder.py (_DECISION_BUILTINS) | Sandbox surface — whitelist of callables the decision_code may use |
Drift in any of these means downstream output will be stale-shaped —
stop and update this SKILL before continuing.
Step 4 — Generate into sandbox
Compute RUN_ID = <YYYYMMDD-HHMMSS>-<role> from current UTC
(date -u +"%Y%m%d-%H%M%S"). Create
tools/scratch/compose-persona/<RUN_ID>/.
4.1 — Invoke author-persona
output_path: <RUN_ROOT>/api/server/personae/<role>/SKILL.md
brief: <persona block from the YAML brief>
domain_display_name: <persona.workflow_label>
default_external_event: <persona.external_event or persona.role + "_decision">
canonical_example_path:
- api/server/personae/finance_bp/SKILL.md (when uses_authority_mcp: true)
- api/server/personae/line_manager/SKILL.md (when uses_authority_mcp: false)
responder_path: api/server/services/persona_responder.py
author-persona writes the SKILL.md to the sandbox path.
4.2 — Write <RUN_ROOT>/REGISTRY-ENTRY.py
"<role>": Persona(
role="<role>",
archetype="<archetype>",
scope_function="<scope_function>",
workflow_label="<workflow_label>",
external_event_default="<external_event>",
scope_business_unit="<scope_business_unit>",
scope_geography="<scope_geography>",
default_authority_band=<None or "<text>">,
uses_authority_mcp=<True | False>,
description="<description>",
),
4.3 — Write <RUN_ROOT>/GRADUATION.md
Human-readable explanation of what graduate.sh does:
- Copy
<RUN_ROOT>/api/server/personae/<role>/SKILL.md →
api/server/personae/<role>/SKILL.md.
- Splice
<RUN_ROOT>/REGISTRY-ENTRY.py content into
api/shared/personas.py PERSONAS dict (between the appropriate
per-domain comment markers — operator's manual call where exactly).
- Add
<role> to PERSONA_AUTO_CLOSE env var ONLY if the demo
profile expects this persona to auto-close gates. Default: do not
add (production-honest default).
- Run
pytest tests/api/shared/test_personas_registry.py to confirm
the registry validates with the new entry.
4.4 — Write <RUN_ROOT>/graduate.sh
Executable script:
#!/usr/bin/env bash
set -euo pipefail
RUN_ROOT="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$RUN_ROOT/../../.." && pwd)"
ROLE="<role>"
SKILL_SRC="$RUN_ROOT/api/server/personae/$ROLE/SKILL.md"
SKILL_DST="$REPO_ROOT/api/server/personae/$ROLE/SKILL.md"
if [ ! -f "$SKILL_SRC" ]; then
echo "ERROR: missing SKILL.md at $SKILL_SRC" >&2
exit 1
fi
mkdir -p "$(dirname "$SKILL_DST")"
cp "$SKILL_SRC" "$SKILL_DST"
echo "wrote $SKILL_DST"
cat <<EOF
Manual splice required:
-----------------------
Open $REPO_ROOT/api/shared/personas.py and paste the entry below
inside the PERSONAS dict, under the matching domain's comment block.
EOF
chmod +x the file.
Step 5 — Self-check + operator handoff
Before returning, verify:
- All four files exist at
<RUN_ROOT>/.
SKILL.md parses as YAML frontmatter + markdown body
(yaml.safe_load(frontmatter_block) works; body has the three
required ## sections from author-persona).
SKILL.md decision_policy block compiles (compile(source, "<test>", "exec") doesn't raise).
REGISTRY-ENTRY.py is valid Python and uses one of the
documented archetype values.
graduate.sh is executable (-x bit set).
Return a one-paragraph summary to the operator naming the four
generated paths and the next-step command (the manual splice + the
pytest command).
Anti-patterns
- Letting the prose paragraph and the executable block disagree. The
prose tracks the code; if they diverge, the brief author needs to
resolve it before this skill runs.
- Generating a persona that uses the authority MCP without setting
uses_authority_mcp: true in the registry entry. The registry flag
is the auditable signal that this persona doesn't inline thresholds.
- Modifying the live
api/shared/personas.py. Always emit the
REGISTRY-ENTRY.py snippet for the operator to splice manually.
Mechanical splicing risks placing the entry under the wrong
per-domain comment block.
- Adding the persona to
PERSONA_AUTO_CLOSE automatically.
Production-honest default: every gate stays open until explicitly
opted in. Demo profiles set the allow-list explicitly.