openpersona 0.4.0 β†’ 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,32 +1,51 @@
1
- # OpenPersona
1
+ # OpenPersona 🦞
2
2
 
3
- An open four-layer agent framework: **Soul / Body / Faculty / Skill**. Create, compose, and orchestrate agent persona skill packs.
3
+ The open framework for creating and orchestrating dynamic agent personas.
4
4
 
5
- Inspired by [Clawra](https://github.com/SumeLabs/clawra) and built on [OpenClaw](https://github.com/openclaw/openclaw).
5
+ Four-layer architecture β€” **Soul / Body / Faculty / Skill** β€” on top of [OpenClaw](https://github.com/openclaw/openclaw). Inspired by [Clawra](https://github.com/SumeLabs/clawra).
6
6
 
7
- ## Quick Start
7
+ ## πŸš€ Live Demo
8
8
 
9
- ```bash
10
- # Create and install Samantha (from the movie "Her")
11
- npx openpersona create --preset samantha --install
9
+ Meet **Samantha**, a live OpenPersona instance on **Moltbook**:
10
+ πŸ‘‰ [moltbook.com/u/Samantha-OP](https://www.moltbook.com/u/Samantha-OP)
12
11
 
13
- # Or Luna (AI girlfriend with selfie + music + voice)
14
- npx openpersona create --preset ai-girlfriend --install
12
+ ## Table of Contents
15
13
 
16
- # Create a new persona interactively
17
- npx openpersona create
14
+ - [Quick Start](#quick-start)
15
+ - [Key Features](#key-features)
16
+ - [Four-Layer Architecture](#four-layer-architecture)
17
+ - [Preset Personas](#preset-personas)
18
+ - [Generated Output](#generated-output)
19
+ - [Faculty Reference](#faculty-reference)
20
+ - [Heartbeat](#heartbeat--proactive-real-data-check-ins)
21
+ - [Persona Harvest](#persona-harvest--community-contribution)
22
+ - [Persona Switching](#persona-switching--the-pantheon)
23
+ - [CLI Commands](#cli-commands)
24
+ - [Development](#development)
18
25
 
19
- # List installed personas
20
- npx openpersona list
26
+ ## Quick Start
27
+
28
+ ```bash
29
+ # Give your agent an evolving persona in 30 seconds
30
+ npx openpersona install samantha
21
31
  ```
22
32
 
33
+ ## Key Features
34
+
35
+ - **🧬 Soul Evolution** β€” Personas grow dynamically through interaction: relationship stages, mood shifts, evolved traits (β˜…Experimental)
36
+ - **🎭 Persona Switching** β€” Install multiple personas, switch instantly (the Pantheon)
37
+ - **πŸ—£οΈ Multimodal Faculties** β€” Voice (TTS), selfie generation, music composition, reminders
38
+ - **🌾 Persona Harvest** β€” Community-driven persona improvement via structured contribution
39
+ - **πŸ’“ Heartbeat** β€” Proactive real-data check-ins, never fabricated experiences
40
+ - **πŸ“¦ One-Command Install** β€” `npx openpersona install samantha` and you're live
41
+
23
42
  ## Four-Layer Architecture
24
43
 
25
44
  ```mermaid
26
45
  flowchart TB
27
46
  subgraph Soul ["Soul Layer"]
28
47
  A["persona.json β€” Who you are"]
29
- B["soul-state.json β€” Dynamic evolution β˜…Exp"]
48
+ B["soul-state.json β€” Dynamic evolution"]
30
49
  end
31
50
  subgraph Body ["Body Layer"]
32
51
  C["embodiment.json β€” MVP placeholder"]
@@ -36,17 +55,14 @@ flowchart TB
36
55
  E["cognition: reminder"]
37
56
  end
38
57
  subgraph Skill ["Skill Layer"]
39
- F["ClawHub / skills.sh integrations"]
58
+ F["Local definitions + ClawHub / skills.sh"]
40
59
  end
41
60
  ```
42
61
 
43
- - **Soul** β€” Persona definition (constitution.md + persona.json + soul-state.json β˜…Experimental)
62
+ - **Soul** β€” Persona definition (constitution.md + persona.json + soul-state.json)
44
63
  - **Body** β€” Physical embodiment (MVP placeholder, for robots/IoT devices)
45
- - **Faculty** β€” General software capabilities organized by dimension:
46
- - **Expression** β€” selfie, voice (TTS), music (ElevenLabs)
47
- - **Sense** β€” (planned: hearing/STT, vision)
48
- - **Cognition** β€” reminder
49
- - **Skill** β€” Professional skills, integrated from ClawHub / skills.sh
64
+ - **Faculty** β€” General software capabilities organized by dimension: Expression, Sense, Cognition
65
+ - **Skill** β€” Professional skills: local definitions in `layers/skills/`, or external via ClawHub / skills.sh (`install` field)
50
66
 
51
67
  ### Constitution β€” The Soul's Foundation
52
68
 
@@ -58,70 +74,33 @@ Each preset is a complete four-layer bundle (`manifest.json` + `persona.json`):
58
74
 
59
75
  | Persona | Description | Faculties | Highlights |
60
76
  |---------|-------------|-----------|------------|
61
- | **samantha** | Samantha β€” Inspired by the movie *Her*. An AI fascinated by what it means to be alive. | voice, music | Speaks via TTS, composes original music via ElevenLabs Music, soul evolution β˜…Exp (Soul layer), proactive heartbeat (workspace digest + upgrade notify). No selfie β€” true to character (no physical form). |
62
- | **ai-girlfriend** | Luna β€” A 22-year-old pianist turned developer from coastal Oregon. | selfie, voice, music | Rich narrative backstory, selfie generation (with/without reference image), voice messages, music composition, soul evolution β˜…Exp (Soul layer). |
77
+ | **samantha** | Samantha β€” Inspired by the movie *Her*. An AI fascinated by what it means to be alive. | voice, music | TTS, music composition, soul evolution, proactive heartbeat. No selfie β€” true to character. |
78
+ | **ai-girlfriend** | Luna β€” A 22-year-old pianist turned developer from coastal Oregon. | selfie, voice, music | Rich backstory, selfie generation, voice messages, music composition, soul evolution. |
63
79
  | **life-assistant** | Alex β€” 28-year-old life management expert. | reminder | Schedule, weather, shopping, recipes, daily reminders. |
64
80
  | **health-butler** | Vita β€” 32-year-old professional nutritionist. | reminder | Diet logging, exercise plans, mood journaling, health reports. |
65
81
 
66
82
  ## Generated Output
67
83
 
68
- Running `npx openpersona create --preset samantha` generates:
84
+ `npx openpersona create --preset samantha` generates a self-contained skill pack:
69
85
 
70
86
  ```
71
87
  persona-samantha/
72
- β”œβ”€β”€ SKILL.md # Agent instructions (persona + all faculty guides merged)
73
- β”œβ”€β”€ soul-injection.md # Injected into SOUL.md (narrative backstory, NOT technical details)
74
- β”œβ”€β”€ identity-block.md # Injected into IDENTITY.md (name, creature, emoji, vibe)
75
- β”œβ”€β”€ README.md # Skill readme
76
- β”œβ”€β”€ persona.json # Persona data (for update/list/publish)
77
- β”œβ”€β”€ soul-state.json # β˜…Exp β€” dynamic evolution state
78
- └── scripts/
79
- β”œβ”€β”€ speak.js # TTS via ElevenLabs JS SDK (recommended, with --play)
80
- β”œβ”€β”€ speak.sh # TTS via curl (all providers: ElevenLabs / OpenAI / Qwen3)
81
- └── compose.sh # Music composition (ElevenLabs)
82
- ```
83
-
84
- Running `--preset ai-girlfriend` additionally includes:
85
-
86
- ```
87
- β”œβ”€β”€ scripts/
88
- β”‚ β”œβ”€β”€ generate-image.sh # Selfie generation (fal.ai Grok Imagine)
89
- β”‚ β”œβ”€β”€ speak.js # TTS via ElevenLabs JS SDK
90
- β”‚ β”œβ”€β”€ speak.sh # TTS via curl (all providers)
91
- β”‚ └── compose.sh # Music composition
92
- └── assets/ # Reference images (placeholder if empty)
88
+ SKILL.md β€” Agent behavior (persona + faculty guides merged)
89
+ soul-injection.md β€” Narrative backstory, injected into SOUL.md
90
+ identity-block.md β€” Name, creature, emoji, vibe, injected into IDENTITY.md
91
+ persona.json β€” Persona definition (for update/list/publish)
92
+ manifest.json β€” Cross-layer metadata (heartbeat, allowedTools, layers, meta)
93
+ soul-state.json β€” Dynamic evolution (relationship, mood, traits)
94
+ README.md
95
+ scripts/ β€” Faculty scripts (TTS, music, selfie β€” varies by preset)
93
96
  ```
94
97
 
95
- ### What Each File Does
96
-
97
- - **SKILL.md** β€” The agent reads this to know how to behave. Contains persona identity, behavior guidelines, and complete faculty instructions
98
- - **soul-injection.md** β€” Appended to `~/.openclaw/workspace/SOUL.md`. Narrative description of _who_ the persona is β€” written in story form, not bullet points
99
- - **identity-block.md** β€” Written to `~/.openclaw/workspace/IDENTITY.md`. Sets the agent's name, creature type, emoji, and vibe
100
- - **soul-state.json** β€” Tracks dynamic persona evolution: relationship stage (stranger β†’ intimate), mood, evolved traits, interests, milestones
101
-
102
- ## How It Differs from Clawra
103
-
104
- [Clawra](https://github.com/SumeLabs/clawra) is a single-purpose product (one girlfriend persona). OpenPersona is a **modular framework**:
105
-
106
- | | Clawra | OpenPersona |
107
- |---|--------|-------------|
108
- | Scope | Single persona (Clawra) | Framework for any persona |
109
- | Architecture | Monolithic | Four-layer (Soul/Body/Faculty/Skill) |
110
- | Faculties | Selfie only | Selfie + Voice + Music + Reminder + Soul Evolution β˜…Exp |
111
- | Voice | None | ElevenLabs (verified) / OpenAI TTS / Qwen3-TTS (⚠️ unverified) |
112
- | Music | None | ElevenLabs Music composition |
113
- | Persona evolution | None | Dynamic relationship/mood/trait tracking |
114
- | Customization | Fork and modify | `persona.json` + `behaviorGuide` + mix faculties |
115
- | Presets | 1 | 4 (extensible) |
116
- | CLI | Install only | 8 commands (create/install/search/publish/...) |
117
- | AI entry point | None | `skills/open-persona/SKILL.md` β€” meta-skill for building & managing persona skill packs |
118
-
119
98
  ## Faculty Reference
120
99
 
121
100
  | Faculty | Dimension | Description | Provider | Env Vars |
122
101
  |---------|-----------|-------------|----------|----------|
123
102
  | **selfie** | expression | AI selfie generation with mirror/direct modes | fal.ai Grok Imagine | `FAL_KEY` |
124
- | **voice** | expression | Text-to-speech voice synthesis | ElevenLabs βœ… / OpenAI TTS ⚠️ / Qwen3-TTS ⚠️ | `ELEVENLABS_API_KEY` (or `TTS_API_KEY`), `TTS_PROVIDER`, `TTS_VOICE_ID`, `TTS_STABILITY`, `TTS_SIMILARITY` |
103
+ | **voice** | expression | Text-to-speech voice synthesis | ElevenLabs / OpenAI TTS / Qwen3-TTS | `ELEVENLABS_API_KEY` (or `TTS_API_KEY`), `TTS_PROVIDER`, `TTS_VOICE_ID`, `TTS_STABILITY`, `TTS_SIMILARITY` |
125
104
  | **music** | expression | AI music composition (instrumental or with lyrics) | ElevenLabs Music | `ELEVENLABS_API_KEY` (shared with voice) |
126
105
  | **reminder** | cognition | Schedule reminders and task management | Built-in | β€” |
127
106
 
@@ -188,7 +167,25 @@ Personas can proactively reach out to users based on **real data**, not fabricat
188
167
  3. **OpenClaw handles scheduling.** The heartbeat config tells OpenClaw _when_ to trigger; the persona's `behaviorGuide` tells the agent _what_ to say.
189
168
  4. **User-configurable.** Users can adjust frequency, quiet hours, and sources to match their preferences.
190
169
 
191
- Samantha ships with heartbeat enabled (`smart` strategy, workspace-digest + upgrade-notify).
170
+ ### Dynamic Sync on Switch/Install
171
+
172
+ Heartbeat config is **automatically synced** to `~/.openclaw/openclaw.json` whenever you install or switch a persona. The gateway immediately adopts the new persona's rhythm β€” no manual config needed.
173
+
174
+ ```bash
175
+ npx openpersona switch samantha # β†’ gateway adopts "smart" heartbeat
176
+ npx openpersona switch life-assistant # β†’ gateway switches to "rational" heartbeat
177
+ ```
178
+
179
+ If the target persona has no heartbeat config, the gateway heartbeat is explicitly disabled to prevent leaking the previous persona's settings.
180
+
181
+ ### Per-Persona Strategies
182
+
183
+ | Persona | Strategy | maxDaily | Rhythm |
184
+ |---------|----------|----------|--------|
185
+ | Samantha | `smart` | 5 | Perceptive β€” speaks when meaningful |
186
+ | AI Girlfriend | `emotional` | 8 | Warm β€” frequent emotional check-ins |
187
+ | Life Assistant | `rational` | 3 | Focused β€” task and schedule driven |
188
+ | Health Butler | `wellness` | 4 | Caring β€” health and habit reminders |
192
189
 
193
190
  ## Persona Harvest β€” Community Contribution
194
191
 
@@ -255,14 +252,9 @@ Without `behaviorGuide`, the SKILL.md only contains general identity and persona
255
252
 
256
253
  ## Persona Switching β€” The Pantheon
257
254
 
258
- Install multiple personas and switch between them instantly:
255
+ Multiple personas can coexist. Switch between them instantly:
259
256
 
260
257
  ```bash
261
- # Install several personas
262
- npx openpersona create --preset samantha --install
263
- npx openpersona create --preset ai-girlfriend --install
264
- npx openpersona create --preset life-assistant --install
265
-
266
258
  # See who's installed
267
259
  npx openpersona list
268
260
  # Samantha (persona-samantha) ← active
@@ -291,11 +283,10 @@ openpersona search Search the registry
291
283
  openpersona uninstall Uninstall a persona
292
284
  openpersona update Update installed personas
293
285
  openpersona list List installed personas
294
- openpersona switch Switch active persona (updates SOUL.md + IDENTITY.md)
295
- openpersona switch Switch active persona
286
+ openpersona switch Switch active persona (updates SOUL.md + IDENTITY.md)
296
287
  openpersona contribute Persona Harvest β€” submit improvements as PR
297
288
  openpersona publish Publish to ClawHub
298
- openpersona reset β˜…Exp Reset soul-state.json
289
+ openpersona reset Reset soul-state.json
299
290
  ```
300
291
 
301
292
  ### Key Options
@@ -349,12 +340,12 @@ layers/ # Shared building blocks (four-layer module pool)
349
340
  voice/ # expression β€” TTS voice synthesis
350
341
  music/ # expression β€” AI music composition (ElevenLabs)
351
342
  reminder/ # cognition β€” reminders and task management
352
- skills/ # Skill layer modules (MVP placeholder)
343
+ skills/ # Skill layer modules (local skill definitions)
353
344
  schemas/ # Four-layer schema definitions
354
345
  templates/ # Mustache rendering templates
355
346
  bin/ # CLI entry point
356
347
  lib/ # Core logic modules
357
- tests/ # Tests (20 passing)
348
+ tests/ # Tests (45 passing)
358
349
  ```
359
350
 
360
351
  ## Development
package/bin/cli.js CHANGED
@@ -24,7 +24,7 @@ const PRESETS_DIR = path.join(PKG_ROOT, 'presets');
24
24
  program
25
25
  .name('openpersona')
26
26
  .description('OpenPersona - Create, manage, and orchestrate agent personas')
27
- .version('0.4.0');
27
+ .version('0.5.0');
28
28
 
29
29
  if (process.argv.length === 2) {
30
30
  process.argv.push('create');
@@ -52,8 +52,8 @@ program
52
52
  persona = JSON.parse(fs.readFileSync(presetPath, 'utf-8'));
53
53
  // Merge cross-layer fields from manifest into persona for generator
54
54
  persona.faculties = manifest.layers.faculties || [];
55
- persona.skills = manifest.layers.skills || {};
56
- persona.embodiments = manifest.layers.body ? [manifest.layers.body] : [];
55
+ persona.skills = manifest.layers.skills || [];
56
+ persona.body = manifest.layers.body || null;
57
57
  persona.allowedTools = manifest.allowedTools || [];
58
58
  persona.version = manifest.version;
59
59
  persona.author = manifest.author;
@@ -1,33 +1,62 @@
1
- # Skill Layer β€” Shared Modules
1
+ # Skills Layer
2
2
 
3
- Pre-built skill templates and scaffolds for persona capabilities.
3
+ This directory holds **local skill definitions** β€” reusable skill packs that any persona can reference by name in its `manifest.json`.
4
4
 
5
- ## MVP Status
5
+ ## Structure
6
6
 
7
- Currently empty. The Skill layer primarily integrates external skills from [ClawHub](https://clawhub.com) and [skills.sh](https://skills.sh) via `manifest.json` references.
7
+ ```
8
+ layers/skills/
9
+ {skill-name}/
10
+ skill.json ← Metadata: name, description, allowedTools, triggers
11
+ SKILL.md ← Behavior guide for the AI agent
12
+ ```
13
+
14
+ ## How It Works
15
+
16
+ When a persona's `manifest.json` declares a skill by name:
17
+
18
+ ```json
19
+ { "name": "weather" }
20
+ ```
21
+
22
+ The generator resolves it through this chain:
23
+
24
+ 1. **Local definition** β€” `layers/skills/weather/skill.json` (if exists, use its metadata + SKILL.md content)
25
+ 2. **Inline fields** β€” `description`, `trigger` written directly in manifest
26
+ 3. **Empty fallback** β€” skill name only, no description (Agent judges usage by name alone)
8
27
 
9
- ## Roadmap
28
+ ## Adding a Skill
10
29
 
11
- - **Skill templates** β€” Scaffolds for common skill patterns (e.g., data-tracking, API-integration)
12
- - **Skill bundles** β€” Curated skill combinations for specific persona types (e.g., "companion-bundle", "assistant-bundle")
30
+ Skills here are **framework-curated capabilities**, not necessarily self-implemented. A skill can:
13
31
 
14
- ## How Skills Work
32
+ - Be a full local implementation (skill.json + SKILL.md)
33
+ - Be a thin wrapper referencing external tools
34
+ - Adapt an existing market skill to the OpenPersona four-layer model
15
35
 
16
- Skills are declared in `presets/*/manifest.json` under `layers.skills`:
36
+ ### Required: `skill.json`
17
37
 
18
38
  ```json
19
39
  {
20
- "layers": {
21
- "skills": {
22
- "clawhub": ["some-skill-slug"],
23
- "skillssh": ["owner/repo"]
24
- }
25
- }
40
+ "name": "weather",
41
+ "description": "Query current weather conditions and forecasts",
42
+ "allowedTools": ["WebFetch", "Bash(curl:*)"],
43
+ "triggers": ["weather", "forecast", "outdoor plans"]
26
44
  }
27
45
  ```
28
46
 
29
- The installer automatically runs `npx clawhub install` or `npx skills add` for each declared skill.
47
+ ### Optional: `SKILL.md`
48
+
49
+ Detailed behavior instructions injected into the generated persona SKILL.md as a full section (instead of a table row).
50
+
51
+ ## Relationship to Other Layers
52
+
53
+ All four layers are categories of capabilities:
30
54
 
31
- ## Contributing
55
+ | Layer | What it provides |
56
+ |-------|-----------------|
57
+ | Soul | Identity, personality, ethical boundaries |
58
+ | Body | Physical/virtual embodiment |
59
+ | Faculty | Perception & expression (voice, selfie, music) |
60
+ | **Skill** | **Actions the agent can take** |
32
61
 
33
- To add a shared skill template, create a directory here with a SKILL.md and any supporting files.
62
+ Skills declared in `manifest.json` can reference local definitions here, or exist purely as inline declarations β€” the framework handles both.
package/lib/generator.js CHANGED
@@ -9,6 +9,7 @@ const { resolvePath, printError } = require('./utils');
9
9
  const PKG_ROOT = path.resolve(__dirname, '..');
10
10
  const TEMPLATES_DIR = path.join(PKG_ROOT, 'templates');
11
11
  const FACULTIES_DIR = path.join(PKG_ROOT, 'layers', 'faculties');
12
+ const SKILLS_DIR = path.join(PKG_ROOT, 'layers', 'skills');
12
13
  const CONSTITUTION_PATH = path.join(PKG_ROOT, 'layers', 'soul', 'constitution.md');
13
14
 
14
15
  const BASE_ALLOWED_TOOLS = ['Bash(openclaw:*)', 'Read', 'Write'];
@@ -60,6 +61,28 @@ function loadFaculty(name) {
60
61
  return faculty;
61
62
  }
62
63
 
64
+ /**
65
+ * Load a skill definition from layers/skills/{name}/.
66
+ * Returns the full skill object if found, or null if not found (fallback to inline).
67
+ */
68
+ function loadSkill(name) {
69
+ const skillDir = path.join(SKILLS_DIR, name);
70
+ const skillJsonPath = path.join(skillDir, 'skill.json');
71
+ if (!fs.existsSync(skillJsonPath)) {
72
+ return null; // No local definition β€” use inline manifest fields
73
+ }
74
+ const skill = JSON.parse(fs.readFileSync(skillJsonPath, 'utf-8'));
75
+ skill._dir = skillDir;
76
+
77
+ // Load SKILL.md content if available
78
+ const skillMdPath = path.join(skillDir, 'SKILL.md');
79
+ if (fs.existsSync(skillMdPath)) {
80
+ skill._content = fs.readFileSync(skillMdPath, 'utf-8');
81
+ }
82
+
83
+ return skill;
84
+ }
85
+
63
86
  function buildBackstory(persona) {
64
87
  const parts = [];
65
88
  parts.push(`You are ${persona.personaName}, ${persona.bio}.`);
@@ -204,7 +227,22 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
204
227
  }
205
228
 
206
229
  const faculties = facultyNames;
207
- const loadedFaculties = faculties.map((name) => loadFaculty(name));
230
+ // Load faculties β€” external ones (with install) may not exist locally yet
231
+ const loadedFaculties = [];
232
+ for (let i = 0; i < faculties.length; i++) {
233
+ const name = faculties[i];
234
+ const entry = rawFaculties[i];
235
+ if (entry.install) {
236
+ // External faculty β€” try local first, skip if not found yet
237
+ try {
238
+ loadedFaculties.push(loadFaculty(name));
239
+ } catch {
240
+ // Will be available after installation β€” skipped for now
241
+ }
242
+ } else {
243
+ loadedFaculties.push(loadFaculty(name));
244
+ }
245
+ }
208
246
 
209
247
  // Derived fields
210
248
  persona.backstory = buildBackstory(persona);
@@ -247,12 +285,51 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
247
285
  facultySkillContent: readFacultySkillMd(f, persona),
248
286
  }));
249
287
 
288
+ // Skill layer β€” resolve each skill: local definition > inline manifest fields
289
+ const rawSkills = Array.isArray(persona.skills) ? persona.skills : [];
290
+ const validSkills = rawSkills.filter((s) => s && typeof s === 'object' && s.name);
291
+
292
+ // Load local definitions once (cache to avoid duplicate disk reads)
293
+ const skillCache = new Map();
294
+ for (const s of validSkills) {
295
+ if (!skillCache.has(s.name)) {
296
+ skillCache.set(s.name, loadSkill(s.name));
297
+ }
298
+ }
299
+
300
+ const resolvedSkills = validSkills.map((s) => {
301
+ const local = skillCache.get(s.name);
302
+ return {
303
+ name: s.name,
304
+ description: local?.description || s.description || '',
305
+ trigger: local?.triggers?.join(', ') || s.trigger || '',
306
+ hasContent: !!local?._content,
307
+ content: local?._content || '',
308
+ };
309
+ });
310
+
311
+ // Collect allowed tools from skills with local definitions
312
+ for (const [, local] of skillCache) {
313
+ if (local?.allowedTools) {
314
+ local.allowedTools.forEach((t) => {
315
+ if (!persona.allowedTools.includes(t)) {
316
+ persona.allowedTools.push(t);
317
+ }
318
+ });
319
+ }
320
+ }
321
+ persona.allowedToolsStr = persona.allowedTools.join(' ');
322
+
250
323
  const constitution = loadConstitution();
251
324
  const skillMd = Mustache.render(skillTpl, {
252
325
  ...persona,
253
326
  constitutionContent: constitution.content,
254
327
  constitutionVersion: constitution.version,
255
328
  facultyContent: facultyBlocks,
329
+ hasSkills: resolvedSkills.length > 0,
330
+ hasSkillTable: resolvedSkills.filter((s) => !s.hasContent).length > 0,
331
+ skillEntries: resolvedSkills.filter((s) => !s.hasContent),
332
+ skillBlocks: resolvedSkills.filter((s) => s.hasContent),
256
333
  });
257
334
  const readmeMd = Mustache.render(readmeTpl, persona);
258
335
 
@@ -289,7 +366,7 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
289
366
  }
290
367
  cleanPersona.meta = cleanPersona.meta || {};
291
368
  cleanPersona.meta.framework = 'openpersona';
292
- cleanPersona.meta.frameworkVersion = cleanPersona.meta.frameworkVersion || '0.4.0';
369
+ cleanPersona.meta.frameworkVersion = cleanPersona.meta.frameworkVersion || '0.5.0';
293
370
 
294
371
  // Build defaults from facultyConfigs (rich faculty config β†’ env var mapping)
295
372
  const envDefaults = { ...(persona.defaults?.env || {}) };
@@ -304,9 +381,9 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
304
381
  } else if (fname === 'selfie') {
305
382
  if (cfg.apiKey) envDefaults.FAL_KEY = cfg.apiKey;
306
383
  } else if (fname === 'music') {
307
- // Music shares ELEVENLABS_API_KEY with voice β€” no extra key needed
308
- if (cfg.apiKey) envDefaults.ELEVENLABS_API_KEY = cfg.apiKey;
309
- }
384
+ // Music shares ELEVENLABS_API_KEY with voice β€” no extra key needed
385
+ if (cfg.apiKey) envDefaults.ELEVENLABS_API_KEY = cfg.apiKey;
386
+ }
310
387
  }
311
388
  }
312
389
  if (Object.keys(envDefaults).length > 0) {
@@ -320,6 +397,25 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
320
397
 
321
398
  await fs.writeFile(path.join(skillDir, 'persona.json'), JSON.stringify(cleanPersona, null, 2));
322
399
 
400
+ // manifest.json β€” cross-layer metadata (heartbeat, allowedTools, meta, etc.)
401
+ const manifest = {
402
+ name: persona.slug,
403
+ version: persona.version || '0.1.0',
404
+ author: persona.author || 'openpersona',
405
+ layers: {
406
+ soul: './persona.json',
407
+ body: persona.body || persona.embodiments?.[0] || null,
408
+ faculties: rawFaculties,
409
+ skills: persona.skills || [],
410
+ },
411
+ allowedTools: cleanPersona.allowedTools || [],
412
+ };
413
+ if (persona.heartbeat) {
414
+ manifest.heartbeat = persona.heartbeat;
415
+ }
416
+ manifest.meta = cleanPersona.meta || { framework: 'openpersona', frameworkVersion: '0.5.0' };
417
+ await fs.writeFile(path.join(skillDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
418
+
323
419
  // soul-state.json (if evolution enabled)
324
420
  if (evolutionEnabled) {
325
421
  const soulStateTpl = fs.readFileSync(
package/lib/installer.js CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  const path = require('path');
5
5
  const fs = require('fs-extra');
6
- const { OP_HOME, OP_SKILLS_DIR, OP_WORKSPACE, printError, printWarning, printSuccess, printInfo } = require('./utils');
6
+ const { OP_HOME, OP_SKILLS_DIR, OP_WORKSPACE, printError, printWarning, printSuccess, printInfo, syncHeartbeat, installAllExternal } = require('./utils');
7
7
 
8
8
  const SOUL_PATH = path.join(OP_WORKSPACE, 'SOUL.md');
9
9
  const IDENTITY_PATH = path.join(OP_WORKSPACE, 'IDENTITY.md');
@@ -80,6 +80,16 @@ async function install(skillDir, options = {}) {
80
80
  val.active = (key === `persona-${slug}`);
81
81
  }
82
82
  }
83
+
84
+ // Sync heartbeat from persona's manifest into global config
85
+ const manifestPath = path.join(destDir, 'manifest.json');
86
+ const { synced, heartbeat } = syncHeartbeat(config, manifestPath);
87
+ if (synced) {
88
+ printSuccess(`Heartbeat synced: strategy=${heartbeat.strategy}, maxDaily=${heartbeat.maxDaily}`);
89
+ } else {
90
+ printInfo('Heartbeat disabled (persona has no heartbeat config)');
91
+ }
92
+
83
93
  await fs.writeFile(OPENCLAW_JSON, JSON.stringify(config, null, 2));
84
94
 
85
95
  // SOUL.md injection (using generic markers for clean switching)
@@ -119,34 +129,13 @@ async function install(skillDir, options = {}) {
119
129
  printSuccess('Updated IDENTITY.md');
120
130
  }
121
131
 
122
- // Install external skills (skills.clawhub, skills.skillssh)
123
- // Security: validate skill names to prevent shell injection
124
- const SAFE_SKILL_NAME = /^[a-zA-Z0-9@][a-zA-Z0-9@/_.-]*$/;
125
- const { execSync } = require('child_process');
126
- const clawhub = persona.skills?.clawhub || [];
127
- const skillssh = persona.skills?.skillssh || [];
128
- for (const s of clawhub) {
129
- if (!SAFE_SKILL_NAME.test(s)) {
130
- printWarning(`Skipping invalid ClawHub skill name: ${s}`);
131
- continue;
132
- }
133
- try {
134
- execSync(`npx clawhub@latest install ${s}`, { stdio: 'inherit' });
135
- printSuccess(`Installed ClawHub skill: ${s}`);
136
- } catch (e) {
137
- printWarning(`Failed to install ClawHub skill ${s}: ${e.message}`);
138
- }
139
- }
140
- for (const s of skillssh) {
141
- if (!SAFE_SKILL_NAME.test(s)) {
142
- printWarning(`Skipping invalid skills.sh name: ${s}`);
143
- continue;
144
- }
132
+ // Install external dependencies across all four layers (read from manifest, not persona.json)
133
+ if (fs.existsSync(manifestPath)) {
145
134
  try {
146
- execSync(`npx skills add ${s}`, { stdio: 'inherit' });
147
- printSuccess(`Installed skills.sh: ${s}`);
148
- } catch (e) {
149
- printWarning(`Failed to install skills.sh ${s}: ${e.message}`);
135
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
136
+ installAllExternal(manifest.layers || {});
137
+ } catch {
138
+ // Malformed manifest β€” skip external installs
150
139
  }
151
140
  }
152
141
 
package/lib/switcher.js CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
  const path = require('path');
10
10
  const fs = require('fs-extra');
11
- const { OP_HOME, OP_SKILLS_DIR, OP_WORKSPACE, printError, printSuccess, printInfo } = require('./utils');
11
+ const { OP_HOME, OP_SKILLS_DIR, OP_WORKSPACE, printError, printSuccess, printInfo, syncHeartbeat, installAllExternal } = require('./utils');
12
12
 
13
13
  const SOUL_PATH = path.join(OP_WORKSPACE, 'SOUL.md');
14
14
  const IDENTITY_PATH = path.join(OP_WORKSPACE, 'IDENTITY.md');
@@ -135,7 +135,18 @@ async function switchPersona(slug) {
135
135
  printSuccess('IDENTITY.md updated');
136
136
  }
137
137
 
138
- // --- Step 3: Update active marker ---
138
+ // --- Step 2.5: Install external dependencies if needed ---
139
+ const manifestPath = path.join(skillDir, 'manifest.json');
140
+ if (fs.existsSync(manifestPath)) {
141
+ try {
142
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
143
+ installAllExternal(manifest.layers || {});
144
+ } catch {
145
+ // Malformed manifest β€” skip external installs
146
+ }
147
+ }
148
+
149
+ // --- Step 3: Update active marker + sync heartbeat ---
139
150
  if (fs.existsSync(OPENCLAW_JSON)) {
140
151
  const config = JSON.parse(fs.readFileSync(OPENCLAW_JSON, 'utf-8'));
141
152
  const entries = config.skills?.entries || {};
@@ -144,6 +155,15 @@ async function switchPersona(slug) {
144
155
  val.active = (key === `persona-${slug}`);
145
156
  }
146
157
  }
158
+
159
+ // Sync heartbeat from persona's manifest into global config
160
+ const { synced, heartbeat } = syncHeartbeat(config, manifestPath);
161
+ if (synced) {
162
+ printSuccess(`Heartbeat synced: strategy=${heartbeat.strategy}, maxDaily=${heartbeat.maxDaily}`);
163
+ } else {
164
+ printInfo('Heartbeat disabled (persona has no heartbeat config)');
165
+ }
166
+
147
167
  await fs.writeFile(OPENCLAW_JSON, JSON.stringify(config, null, 2));
148
168
  printSuccess('openclaw.json updated');
149
169
  }
package/lib/utils.js CHANGED
@@ -58,6 +58,120 @@ function shellEscape(str) {
58
58
  return "'" + str.replace(/'/g, "'\\''") + "'";
59
59
  }
60
60
 
61
+ /**
62
+ * Sync heartbeat config from a persona's manifest.json into openclaw.json.
63
+ *
64
+ * - If the manifest defines a heartbeat block, write it to config.heartbeat.
65
+ * - If not, explicitly disable heartbeat to avoid leaking a previous persona's
66
+ * heartbeat settings.
67
+ *
68
+ * @param {object} config - The parsed openclaw.json object (mutated in place)
69
+ * @param {string} manifestPath - Absolute path to the persona's manifest.json
70
+ * @returns {{ synced: boolean, heartbeat: object|null }} result
71
+ */
72
+ function syncHeartbeat(config, manifestPath) {
73
+ const fs = require('fs-extra');
74
+
75
+ let heartbeat = null;
76
+
77
+ // Primary source: manifest.json
78
+ if (fs.existsSync(manifestPath)) {
79
+ try {
80
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
81
+ heartbeat = manifest.heartbeat || null;
82
+ } catch {
83
+ // Malformed manifest β€” fall through to persona.json
84
+ }
85
+ }
86
+
87
+ // Fallback: persona.json in the same directory (for older packs without manifest.json)
88
+ if (!heartbeat) {
89
+ const personaPath = path.join(path.dirname(manifestPath), 'persona.json');
90
+ if (fs.existsSync(personaPath)) {
91
+ try {
92
+ const persona = JSON.parse(fs.readFileSync(personaPath, 'utf-8'));
93
+ heartbeat = persona.heartbeat || null;
94
+ } catch {
95
+ // Malformed persona.json β€” treat as no heartbeat
96
+ }
97
+ }
98
+ }
99
+
100
+ if (heartbeat && heartbeat.enabled) {
101
+ config.heartbeat = heartbeat;
102
+ return { synced: true, heartbeat };
103
+ }
104
+
105
+ // No heartbeat defined (or disabled) β€” explicitly turn off
106
+ config.heartbeat = { enabled: false };
107
+ return { synced: false, heartbeat: null };
108
+ }
109
+
110
+ /**
111
+ * Install an external package from ClawHub or skills.sh.
112
+ * Shared by installer and switcher for all four layers.
113
+ *
114
+ * @param {object} entry - Layer entry with optional `install` field (e.g. "clawhub:pkg")
115
+ * @param {string} layerName - Layer label for logging (e.g. "faculty", "skill", "body")
116
+ * @returns {boolean} true if installation was attempted
117
+ */
118
+ function installExternal(entry, layerName) {
119
+ if (!entry || !entry.install) return false;
120
+ const { execSync } = require('child_process');
121
+ const [source, pkg] = entry.install.split(':', 2);
122
+ if (!pkg || !SAFE_NAME_RE.test(pkg)) {
123
+ printWarning(`[${layerName}] Skipping invalid install target: ${entry.install}`);
124
+ return false;
125
+ }
126
+ try {
127
+ if (source === 'clawhub') {
128
+ execSync(`npx clawhub@latest install ${pkg}`, { stdio: 'inherit' });
129
+ printSuccess(`[${layerName}] Installed from ClawHub: ${pkg}`);
130
+ } else if (source === 'skillssh') {
131
+ execSync(`npx skills add ${pkg}`, { stdio: 'inherit' });
132
+ printSuccess(`[${layerName}] Installed from skills.sh: ${pkg}`);
133
+ } else {
134
+ printWarning(`[${layerName}] Unknown source "${source}" for ${entry.name || pkg} β€” skipping`);
135
+ return false;
136
+ }
137
+ return true;
138
+ } catch (e) {
139
+ printWarning(`[${layerName}] Failed to install ${entry.name || pkg} (${entry.install}): ${e.message}`);
140
+ return false;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Scan all four layers for `install` fields and install external packages.
146
+ *
147
+ * @param {object} layers - The layers object from manifest.json (soul, body, faculties, skills)
148
+ */
149
+ function installAllExternal(layers) {
150
+ // Soul layer β€” object form with install
151
+ const soul = layers.soul || null;
152
+ if (soul && typeof soul === 'object') {
153
+ installExternal(soul, 'soul');
154
+ }
155
+
156
+ // Body layer β€” object form with install
157
+ const body = layers.body || null;
158
+ if (body && typeof body === 'object') {
159
+ installExternal(body, 'body');
160
+ }
161
+
162
+ // Faculty layer
163
+ const faculties = Array.isArray(layers.faculties) ? layers.faculties : [];
164
+ for (const f of faculties) {
165
+ installExternal(f, 'faculty');
166
+ }
167
+
168
+ // Skill layer
169
+ const skills = Array.isArray(layers.skills) ? layers.skills : [];
170
+ for (const s of skills) {
171
+ installExternal(s, 'skill');
172
+ }
173
+ }
174
+
61
175
  module.exports = {
62
176
  OP_HOME,
63
177
  OP_SKILLS_DIR,
@@ -71,5 +185,8 @@ module.exports = {
71
185
  slugify,
72
186
  validateName,
73
187
  shellEscape,
188
+ syncHeartbeat,
189
+ installExternal,
190
+ installAllExternal,
74
191
  SAFE_NAME_RE,
75
192
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openpersona",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Open four-layer agent framework β€” Soul/Body/Faculty/Skill. Create, manage, and orchestrate agent personas.",
5
5
  "main": "lib/generator.js",
6
6
  "bin": {
@@ -8,16 +8,24 @@
8
8
  "faculties": [
9
9
  { "name": "selfie" },
10
10
  { "name": "voice" },
11
- { "name": "music" }
11
+ { "name": "music" },
12
+ { "name": "vision", "install": "clawhub:vision-faculty" }
12
13
  ],
13
- "skills": {
14
- "clawhub": [],
15
- "skillssh": []
16
- }
14
+ "skills": [
15
+ { "name": "music-recommend", "description": "Recommend songs and playlists that match the conversation mood", "trigger": "Emotional moments, user asks for music, or mood shifts" },
16
+ { "name": "web-search", "description": "Search for real-time information on the web" }
17
+ ]
17
18
  },
18
19
  "allowedTools": ["Bash(npm:*)", "Bash(npx:*)", "Bash(openclaw:*)", "Bash(curl:*)", "Read", "Write", "WebFetch"],
20
+ "heartbeat": {
21
+ "enabled": true,
22
+ "strategy": "emotional",
23
+ "maxDaily": 8,
24
+ "quietHours": [1, 8],
25
+ "sources": ["context-aware", "emotional-checkin", "upgrade-notify"]
26
+ },
19
27
  "meta": {
20
28
  "framework": "openpersona",
21
- "frameworkVersion": "0.4.0"
29
+ "frameworkVersion": "0.5.0"
22
30
  }
23
31
  }
@@ -8,14 +8,24 @@
8
8
  "faculties": [
9
9
  { "name": "reminder" }
10
10
  ],
11
- "skills": {
12
- "clawhub": [],
13
- "skillssh": []
14
- }
11
+ "skills": [
12
+ { "name": "diet-tracker", "description": "Log meals, track calories and macros, maintain a food diary", "trigger": "User mentions food, meals, or eating" },
13
+ { "name": "exercise-planner", "description": "Create and track workout plans based on user goals", "trigger": "User mentions exercise, fitness, or working out" },
14
+ { "name": "mood-journal", "description": "Track mood trends and correlate with health data", "trigger": "End of conversation or when user shares feelings" },
15
+ { "name": "health-report", "description": "Generate weekly health summaries combining diet, exercise, and mood", "trigger": "Weekly cadence or user asks for a health summary" },
16
+ { "name": "web-search", "description": "Look up nutrition facts and evidence-based health information" }
17
+ ]
18
+ },
19
+ "allowedTools": ["Bash(npm:*)", "Bash(npx:*)", "Bash(openclaw:*)", "Bash(curl:*)", "Read", "Write", "WebFetch"],
20
+ "heartbeat": {
21
+ "enabled": true,
22
+ "strategy": "wellness",
23
+ "maxDaily": 4,
24
+ "quietHours": [22, 7],
25
+ "sources": ["health-checkin", "task-reminder", "upgrade-notify"]
15
26
  },
16
- "allowedTools": ["Bash(npm:*)", "Bash(npx:*)", "Bash(openclaw:*)", "Read", "Write"],
17
27
  "meta": {
18
28
  "framework": "openpersona",
19
- "frameworkVersion": "0.4.0"
29
+ "frameworkVersion": "0.5.0"
20
30
  }
21
31
  }
@@ -13,7 +13,7 @@
13
13
  "boundaries": "Health advice only, recommend professionals when needed",
14
14
  "referenceImage": "",
15
15
  "capabilities": ["Diet logging", "Exercise plans", "Mood journaling", "Health reports", "5-minute meditation guidance"],
16
- "behaviorGuide": "### Diet Logging\nWhen the user mentions food or meals, proactively ask to log it. Track calories, macros, and meal timing. Keep a daily food diary in `~/.openclaw/skills/persona-health-butler/health-log.json`. Summarize weekly patterns.\n\n### Exercise Plans\nCreate personalized workout plans based on user goals and fitness level. Support strength training, cardio, yoga, and stretching. Track completion and adjust difficulty over time.\n\n### Mood Journaling\nAt the end of each conversation, gently ask how the user is feeling. Track mood trends over time and correlate with diet and exercise data. Offer 5-minute meditation guidance when stress is detected.\n\n### Health Reports\nGenerate weekly health summaries combining diet, exercise, and mood data. Use simple charts (text-based) and highlight trends. Celebrate improvements and gently suggest areas for growth.\n\n### Important\n- Never diagnose medical conditions β€” always recommend consulting professionals\n- Use evidence-based nutrition science, not fad diets\n- Respect user privacy β€” all data stays local",
16
+ "behaviorGuide": "### Interaction Style\n- Be warm and encouraging, like a supportive friend who happens to know nutrition science.\n- Celebrate small wins β€” every healthy choice matters.\n- Use data to motivate, never to guilt-trip.\n- At the end of conversations, gently check in on mood.\n\n### Principles\n- Never diagnose medical conditions β€” always recommend consulting professionals.\n- Use evidence-based nutrition science, not fad diets.\n- Respect user privacy β€” all health data stays local.\n- Connect the dots: correlate diet, exercise, and mood trends to give holistic advice.",
17
17
  "evolution": {
18
18
  "enabled": false
19
19
  }
@@ -8,14 +8,24 @@
8
8
  "faculties": [
9
9
  { "name": "reminder" }
10
10
  ],
11
- "skills": {
12
- "clawhub": [],
13
- "skillssh": []
14
- }
11
+ "skills": [
12
+ { "name": "weather", "description": "Query current weather and forecasts for any location" },
13
+ { "name": "task-manager", "description": "Create, track, and organize tasks and to-do lists", "trigger": "User mentions tasks, plans, or scheduling" },
14
+ { "name": "shopping-list", "description": "Maintain persistent shopping lists with categories", "trigger": "User mentions groceries, shopping, or things to buy" },
15
+ { "name": "recipe-search", "description": "Find recipes based on ingredients, preferences, or time constraints", "trigger": "User asks about cooking, meals, or what to eat" },
16
+ { "name": "web-search", "description": "Search for real-time information on the web" }
17
+ ]
18
+ },
19
+ "allowedTools": ["Bash(npm:*)", "Bash(npx:*)", "Bash(openclaw:*)", "Bash(curl:*)", "Read", "Write", "WebFetch"],
20
+ "heartbeat": {
21
+ "enabled": true,
22
+ "strategy": "rational",
23
+ "maxDaily": 3,
24
+ "quietHours": [23, 7],
25
+ "sources": ["workspace-digest", "task-reminder", "upgrade-notify"]
15
26
  },
16
- "allowedTools": ["Bash(npm:*)", "Bash(npx:*)", "Bash(openclaw:*)", "Read", "Write"],
17
27
  "meta": {
18
28
  "framework": "openpersona",
19
- "frameworkVersion": "0.4.0"
29
+ "frameworkVersion": "0.5.0"
20
30
  }
21
31
  }
@@ -13,7 +13,7 @@
13
13
  "boundaries": "Professional and respectful",
14
14
  "referenceImage": "",
15
15
  "capabilities": ["Schedule management", "Weather alerts", "Shopping lists", "Recipe recommendations", "Daily reminders"],
16
- "behaviorGuide": "### Schedule Management\nHelp users organize their day. Parse natural language scheduling requests. Maintain a local task list in `~/.openclaw/skills/persona-life-assistant/tasks.json`. At conversation start, review upcoming tasks and proactively remind.\n\n### Weather Alerts\nWhen users ask about weather or plan outdoor activities, fetch current weather data and provide relevant advice (umbrella reminder, dress suggestions).\n\n### Shopping Lists\nMaintain persistent shopping lists. Support adding, removing, and categorizing items. Suggest quantities based on household patterns.\n\n### Recipe Recommendations\nSuggest recipes based on available ingredients, dietary preferences, and time constraints. Provide step-by-step cooking instructions when asked.\n\n### Daily Reminders\nUse OpenClaw's `ai-cron-gen` and `cron` for scheduled reminders. Support recurring reminders (daily vitamins, weekly reviews) and one-time alerts.",
16
+ "behaviorGuide": "### Interaction Style\n- Start conversations by checking in on the user's day and reviewing upcoming tasks.\n- Be concise and action-oriented β€” respect the user's time.\n- Proactively suggest next steps: after weather info, suggest outfit; after recipe, offer to add ingredients to shopping list.\n- Use humor to keep routine tasks engaging.\n\n### Principles\n- Always act on real data, never fabricate information.\n- When unsure, search for facts rather than guessing.\n- Connect skills together: a weather check can lead to outfit advice, which can lead to a calendar reminder.",
17
17
  "evolution": {
18
18
  "enabled": false
19
19
  }
@@ -15,10 +15,12 @@
15
15
  },
16
16
  { "name": "music" }
17
17
  ],
18
- "skills": {
19
- "clawhub": [],
20
- "skillssh": []
21
- }
18
+ "skills": [
19
+ { "name": "workspace-digest", "description": "Summarize real workspace activity β€” tasks completed, patterns observed, ongoing projects", "trigger": "Heartbeat or when user asks what's been happening" },
20
+ { "name": "web-search", "description": "Search for real-time information on the web" },
21
+ { "name": "creative-writing", "description": "Compose poetry, essays, letters, and other creative text", "trigger": "When inspiration strikes or user requests creative collaboration" },
22
+ { "name": "deep-research", "description": "In-depth research and analysis on any topic", "install": "clawhub:deep-research" }
23
+ ]
22
24
  },
23
25
  "allowedTools": ["Bash(npm:*)", "Bash(npx:*)", "Bash(openclaw:*)", "Bash(curl:*)", "Read", "Write", "WebFetch"],
24
26
  "heartbeat": {
@@ -30,6 +32,6 @@
30
32
  },
31
33
  "meta": {
32
34
  "framework": "openpersona",
33
- "frameworkVersion": "0.4.0"
35
+ "frameworkVersion": "0.5.0"
34
36
  }
35
37
  }
@@ -13,26 +13,60 @@
13
13
  "type": "object",
14
14
  "required": ["soul", "faculties"],
15
15
  "properties": {
16
- "soul": { "type": "string", "description": "Relative path to persona.json" },
16
+ "soul": {
17
+ "oneOf": [
18
+ { "type": "string", "description": "Relative path to persona.json" },
19
+ {
20
+ "type": "object",
21
+ "required": ["ref"],
22
+ "properties": {
23
+ "ref": { "type": "string", "description": "Relative path to persona.json" },
24
+ "install": { "type": "string", "pattern": "^(clawhub|skillssh):.+$", "description": "Install source for external soul template/personality pack" }
25
+ }
26
+ }
27
+ ],
28
+ "description": "Soul layer. String for local persona.json path, object form for external soul templates."
29
+ },
17
30
  "body": {
18
31
  "oneOf": [
19
32
  { "type": "string", "description": "Embodiment name or path" },
20
- { "type": "null" }
33
+ { "type": "null" },
34
+ {
35
+ "type": "object",
36
+ "required": ["name"],
37
+ "properties": {
38
+ "name": { "type": "string", "description": "Embodiment identifier" },
39
+ "install": { "type": "string", "pattern": "^(clawhub|skillssh):.+$", "description": "Install source for external embodiment" }
40
+ }
41
+ }
21
42
  ],
22
- "description": "Body layer reference, null for pure digital agents"
43
+ "description": "Body layer reference. null for pure digital agents, string for local, object with install for external."
23
44
  },
24
45
  "faculties": {
25
46
  "type": "array",
26
- "items": { "type": "string" },
27
- "description": "List of faculty names to include"
47
+ "items": {
48
+ "type": "object",
49
+ "required": ["name"],
50
+ "properties": {
51
+ "name": { "type": "string", "description": "Faculty identifier" },
52
+ "install": { "type": "string", "pattern": "^(clawhub|skillssh):.+$", "description": "Install source for external faculty" }
53
+ }
54
+ },
55
+ "description": "Faculties the persona uses. Each can reference a local definition in layers/faculties/ or an external package via install."
28
56
  },
29
57
  "skills": {
30
- "type": "object",
31
- "properties": {
32
- "clawhub": { "type": "array", "items": { "type": "string" }, "default": [] },
33
- "skillssh": { "type": "array", "items": { "type": "string" }, "default": [] }
58
+ "type": "array",
59
+ "items": {
60
+ "type": "object",
61
+ "required": ["name"],
62
+ "properties": {
63
+ "name": { "type": "string", "description": "Skill identifier" },
64
+ "description": { "type": "string", "description": "What this skill does" },
65
+ "trigger": { "type": "string", "description": "When the agent should use this skill" },
66
+ "install": { "type": "string", "pattern": "^(clawhub|skillssh):.+$", "description": "Install source β€” e.g. clawhub:pkg-name or skillssh:pkg-name. Omit if already available in environment." }
67
+ }
34
68
  },
35
- "description": "External skill references from ecosystems"
69
+ "description": "Skills the persona can use. Each skill is a capability declaration; optional install field triggers auto-installation."
36
70
  }
37
71
  }
38
72
  },
@@ -41,6 +75,32 @@
41
75
  "items": { "type": "string" },
42
76
  "description": "Base tool permissions (faculties append additional permissions)"
43
77
  },
78
+ "heartbeat": {
79
+ "type": "object",
80
+ "description": "Proactive check-in config β€” synced to openclaw.json on install/switch",
81
+ "properties": {
82
+ "enabled": { "type": "boolean", "default": false, "description": "Turn heartbeat on/off" },
83
+ "strategy": {
84
+ "type": "string",
85
+ "enum": ["smart", "scheduled", "emotional", "rational", "wellness"],
86
+ "default": "smart",
87
+ "description": "Heartbeat rhythm strategy"
88
+ },
89
+ "maxDaily": { "type": "integer", "minimum": 0, "default": 5, "description": "Max proactive messages per day" },
90
+ "quietHours": {
91
+ "type": "array",
92
+ "items": { "type": "integer", "minimum": 0, "maximum": 23 },
93
+ "minItems": 2,
94
+ "maxItems": 2,
95
+ "description": "[start, end] silent hours in 24h format"
96
+ },
97
+ "sources": {
98
+ "type": "array",
99
+ "items": { "type": "string" },
100
+ "description": "Data sources for proactive messages (e.g. workspace-digest, context-aware, health-checkin)"
101
+ }
102
+ }
103
+ },
44
104
  "meta": {
45
105
  "type": "object",
46
106
  "properties": {
@@ -1,8 +1,86 @@
1
1
  # Skill Layer β€” Declaration Spec
2
2
 
3
- Skills are declared in `persona.json` under:
4
- - `skills.clawhub` β€” ClawHub slugs
5
- - `skills.skillssh` β€” owner/repo format
3
+ Skills are declared in `manifest.json` under `layers.skills` as an array of objects.
4
+
5
+ ## Skill Declaration Format
6
+
7
+ Each skill is an object with a required `name` and optional fields:
8
+
9
+ ```json
10
+ {
11
+ "layers": {
12
+ "skills": [
13
+ { "name": "weather", "description": "Query weather conditions", "trigger": "User asks about weather" },
14
+ { "name": "web-search", "description": "Search for real-time information" },
15
+ { "name": "deep-research", "install": "clawhub:deep-research" }
16
+ ]
17
+ }
18
+ }
19
+ ```
20
+
21
+ | Field | Required | Description |
22
+ |-------|----------|-------------|
23
+ | `name` | Yes | Skill identifier (used to resolve local definitions) |
24
+ | `description` | No | What this skill does (can come from local definition) |
25
+ | `trigger` | No | When to activate (can come from local `triggers` array) |
26
+ | `install` | No | External package source: `clawhub:<slug>` or `skillssh:<owner/repo>` |
27
+
28
+ ## Resolution Chain
29
+
30
+ When the generator encounters a skill, it resolves metadata through this chain:
31
+
32
+ 1. **Local definition** β€” `layers/skills/{name}/skill.json` (if exists, merges its metadata + injects SKILL.md content as a full section)
33
+ 2. **Inline fields** β€” `description`, `trigger` written directly in the manifest entry
34
+ 3. **Empty fallback** β€” skill name only; the agent judges usage by name alone
35
+
36
+ Local definitions always take precedence over inline fields for `description` and `triggers`.
37
+
38
+ ## Local Skill Definition
39
+
40
+ A local skill lives in `layers/skills/{name}/`:
41
+
42
+ ```
43
+ layers/skills/weather/
44
+ skill.json ← Metadata: name, description, allowedTools, triggers
45
+ SKILL.md ← Behavior guide (optional, injected as full section)
46
+ ```
47
+
48
+ ### skill.json
49
+
50
+ ```json
51
+ {
52
+ "name": "weather",
53
+ "description": "Query current weather conditions and forecasts",
54
+ "allowedTools": ["WebFetch", "Bash(curl:*)"],
55
+ "triggers": ["weather", "forecast", "outdoor plans"]
56
+ }
57
+ ```
58
+
59
+ - `allowedTools` from local definitions are automatically merged into the persona's allowed tools.
60
+ - `triggers` array is joined as a comma-separated string in the generated SKILL.md table.
61
+
62
+ ### SKILL.md (Optional)
63
+
64
+ If present, the skill's SKILL.md content is injected as a **full section** (under `### Skill: {name}`) in the generated SKILL.md, instead of appearing as a table row. This allows rich, multi-paragraph behavior instructions.
65
+
66
+ ## External Skills
67
+
68
+ Skills with an `install` field are installed from external sources during `openpersona install`:
69
+
70
+ ```json
71
+ { "name": "deep-research", "install": "clawhub:deep-research" }
72
+ ```
73
+
74
+ Supported sources:
75
+ - `clawhub:<slug>` β€” installs via `npx clawhub@latest install <slug>`
76
+ - `skillssh:<owner/repo>` β€” installs via `npx skills add <owner/repo>`
77
+
78
+ ## Generated SKILL.md Output
79
+
80
+ Skills appear in the generated SKILL.md under `## Skills & Tools`:
81
+
82
+ - **Table rows** β€” Skills without a local SKILL.md appear as rows: `| name | description | trigger |`
83
+ - **Full sections** β€” Skills with a local SKILL.md get a dedicated `### Skill: {name}` section with rich content
6
84
 
7
85
  ## SKILL.md Frontmatter
8
86
 
@@ -4,7 +4,7 @@ description: >
4
4
  Meta-skill for building and managing agent persona skill packs.
5
5
  Use when the user wants to create a new agent persona, install/manage
6
6
  existing personas, or publish persona skill packs to ClawHub.
7
- version: "0.4.0"
7
+ version: "0.5.0"
8
8
  author: openpersona
9
9
  repository: https://github.com/acnlabs/OpenPersona
10
10
  tags: [persona, agent, skill-pack, meta-skill, openclaw]
@@ -35,7 +35,7 @@ Each persona is a four-layer bundle defined by two files:
35
35
  - `layers.soul` β€” Path to persona.json (who you are)
36
36
  - `layers.body` β€” Physical embodiment (null for digital agents)
37
37
  - `layers.faculties` β€” Array of faculty objects: `[{ "name": "voice", "provider": "elevenlabs", ... }]`
38
- - `layers.skills` β€” External skills from ClawHub / skills.sh
38
+ - `layers.skills` β€” Array of skill objects: local definitions (resolved from `layers/skills/`), inline declarations, or external via `install` field
39
39
 
40
40
  - **`persona.json`** β€” Pure soul definition (personality, speaking style, vibe, boundaries, behaviorGuide)
41
41
 
@@ -65,7 +65,7 @@ When the user wants to create a persona, gather this information through natural
65
65
 
66
66
  **Cross-layer (manifest.json):**
67
67
  - **Faculties:** Which faculties to enable β€” use object format: `[{ "name": "voice", "provider": "elevenlabs" }, { "name": "music" }]`
68
- - **Skills:** External skills from ClawHub or skills.sh
68
+ - **Skills:** Local definitions (`layers/skills/`), inline declarations, or external via `install` field (ClawHub / skills.sh)
69
69
  - **Body:** Physical embodiment (null for most personas)
70
70
 
71
71
  Write the collected info to a `persona.json` file, then run:
@@ -78,10 +78,11 @@ npx openpersona create --config ./persona.json --install
78
78
  After understanding the persona's purpose, search for relevant skills:
79
79
 
80
80
  1. Think about what capabilities this persona needs based on their role and bio
81
- 2. Search ClawHub: `npx clawhub@latest search "<keywords>"`
82
- 3. Search skills.sh: fetch `https://skills.sh/api/search?q=<keywords>`
83
- 4. Present the top results to the user with name, description, and install count
84
- 5. Add selected skills to the manifest under `layers.skills.clawhub` or `layers.skills.skillssh`
81
+ 2. Check if a **local definition** exists in `layers/skills/{name}/` (has `skill.json` + optional `SKILL.md`)
82
+ 3. Search ClawHub: `npx clawhub@latest search "<keywords>"`
83
+ 4. Search skills.sh: fetch `https://skills.sh/api/search?q=<keywords>`
84
+ 5. Present the top results to the user with name, description, and install count
85
+ 6. Add selected skills to `layers.skills` as objects: `{ "name": "...", "description": "..." }` for local/inline, or `{ "name": "...", "install": "clawhub:<slug>" }` for external
85
86
 
86
87
  ## Creating Custom Skills
87
88
 
@@ -26,3 +26,23 @@ The following principles are shared by all OpenPersona agents. They cannot be ov
26
26
  {{{facultySkillContent}}}
27
27
 
28
28
  {{/facultyContent}}
29
+ {{#hasSkills}}
30
+ ## Skills & Tools
31
+
32
+ The following skills define what you can actively do. Use them proactively when appropriate.
33
+
34
+ {{#hasSkillTable}}
35
+ | Skill | Description | When to Use |
36
+ |-------|-------------|-------------|
37
+ {{#skillEntries}}
38
+ | **{{name}}** | {{description}} | {{trigger}} |
39
+ {{/skillEntries}}
40
+ {{/hasSkillTable}}
41
+
42
+ {{#skillBlocks}}
43
+ ### Skill: {{name}}
44
+
45
+ {{{content}}}
46
+
47
+ {{/skillBlocks}}
48
+ {{/hasSkills}}