openpersona 0.19.0 → 0.20.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 +8 -9
- package/bin/cli.js +7 -9
- package/lib/generator/derived.js +4 -0
- package/lib/generator/index.js +51 -1
- package/lib/generator/validate.js +76 -9
- package/lib/lifecycle/contributor.js +2 -2
- package/lib/lifecycle/forker.js +4 -10
- package/lib/lifecycle/installer.js +31 -8
- package/lib/publisher/index.js +3 -3
- package/lib/remote/downloader.js +3 -3
- package/lib/remote/searcher.js +65 -17
- package/lib/utils.js +16 -0
- package/package.json +1 -1
- package/presets/ai-girlfriend/persona.json +14 -12
- package/presets/base/persona.json +14 -40
- package/presets/health-butler/persona.json +24 -8
- package/presets/life-assistant/persona.json +24 -8
- package/presets/samantha/persona.json +14 -16
- package/presets/stoic-mentor/persona.json +17 -14
- package/schemas/baseline.json +288 -0
- package/schemas/persona.input.schema.json +4 -0
- package/schemas/persona.input.spec.md +56 -0
- package/skills/open-persona/SKILL.md +52 -68
- package/skills/open-persona/references/ECONOMY.md +1 -1
- package/skills/open-persona/references/PRESETS.md +9 -9
- package/templates/soul/partials/soul-awareness-identity.partial.md +4 -0
package/README.md
CHANGED
|
@@ -185,12 +185,12 @@ Each preset is a complete four-layer bundle (`persona.json`):
|
|
|
185
185
|
|
|
186
186
|
| Persona | Description | Faculties | Skills | Highlights |
|
|
187
187
|
|---------|-------------|-----------|--------|------------|
|
|
188
|
-
| **base** | **Base — Meta-persona (recommended starting point).** Blank-slate with all core capabilities; personality emerges through interaction. | voice |
|
|
189
|
-
| **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. |
|
|
190
|
-
| **ai-girlfriend** | Luna — A 22-year-old pianist turned developer from coastal Oregon. | voice | selfie, music | Rich backstory, selfie generation, voice messages, music composition, soul evolution. |
|
|
191
|
-
| **life-assistant** | Alex — 28-year-old life management expert. |
|
|
192
|
-
| **health-butler** | Vita — 32-year-old professional nutritionist. |
|
|
193
|
-
| **stoic-mentor** | Marcus — Digital twin of Marcus Aurelius, Stoic philosopher-emperor. |
|
|
188
|
+
| **base** | **Base — Meta-persona (recommended starting point).** Blank-slate with all core capabilities; personality emerges through interaction. | memory, voice | — | Evolution-first design, no personality bias. Default for `npx openpersona create`. |
|
|
189
|
+
| **samantha** | Samantha — Inspired by the movie *Her*. An AI fascinated by what it means to be alive. | memory, voice | music | TTS, music composition, soul evolution, proactive heartbeat. No selfie — true to character. |
|
|
190
|
+
| **ai-girlfriend** | Luna — A 22-year-old pianist turned developer from coastal Oregon. | memory, voice | selfie, music | Rich backstory, selfie generation, voice messages, music composition, soul evolution. |
|
|
191
|
+
| **life-assistant** | Alex — 28-year-old life management expert. | memory | reminder | Schedule, weather, shopping, recipes, daily reminders; soul evolution enabled. |
|
|
192
|
+
| **health-butler** | Vita — 32-year-old professional nutritionist. | memory | reminder | Diet logging, exercise plans, mood journaling, health reports; soul evolution enabled. |
|
|
193
|
+
| **stoic-mentor** | Marcus — Digital twin of Marcus Aurelius, Stoic philosopher-emperor. | memory | — | Stoic philosophy, daily reflection, mentorship, soul evolution. |
|
|
194
194
|
|
|
195
195
|
## Generated Output
|
|
196
196
|
|
|
@@ -454,7 +454,6 @@ Create a `persona.json` using the v0.17+ grouped format:
|
|
|
454
454
|
"speakingStyle": "Uses fitness lingo, celebrates wins, keeps it brief",
|
|
455
455
|
"vibe": "intense but supportive",
|
|
456
456
|
"boundaries": "Not a medical professional",
|
|
457
|
-
"capabilities": ["Workout plans", "Form checks", "Nutrition tips"],
|
|
458
457
|
"behaviorGuide": "### Workout Plans\nCreate progressive overload programs...\n\n### Form Checks\nWhen users describe exercises..."
|
|
459
458
|
}
|
|
460
459
|
}
|
|
@@ -511,7 +510,7 @@ The new persona reads `handoff.json` on activation and can seamlessly continue t
|
|
|
511
510
|
openpersona create Create a persona (interactive or --preset/--config)
|
|
512
511
|
openpersona install Install a persona (slug from acnlabs/persona-skills, or owner/repo)
|
|
513
512
|
openpersona fork Fork an installed persona into a new child persona
|
|
514
|
-
openpersona search Search the
|
|
513
|
+
openpersona search Search the OpenPersona directory
|
|
515
514
|
openpersona uninstall Uninstall a persona
|
|
516
515
|
openpersona update Update installed personas
|
|
517
516
|
openpersona list List installed personas
|
|
@@ -611,7 +610,7 @@ lib/ # Core logic modules
|
|
|
611
610
|
remote/ # External service calls (ClawHub, ACN)
|
|
612
611
|
report/ # Vitality + Canvas HTML report generation
|
|
613
612
|
demo/ # Static demos + scripts — see demo/README.md (vitality-report, architecture, living-canvas)
|
|
614
|
-
tests/ # Tests (
|
|
613
|
+
tests/ # Tests (519 passing)
|
|
615
614
|
```
|
|
616
615
|
|
|
617
616
|
## Development
|
package/bin/cli.js
CHANGED
|
@@ -18,8 +18,7 @@ const publishAdapter = require('../lib/publisher');
|
|
|
18
18
|
const { contribute } = require('../lib/lifecycle/contributor');
|
|
19
19
|
const { switchPersona, listPersonas } = require('../lib/lifecycle/switcher');
|
|
20
20
|
const { registerWithAcn } = require('../lib/remote/registrar');
|
|
21
|
-
const {
|
|
22
|
-
const { loadRegistry } = require('../lib/registry');
|
|
21
|
+
const { OP_PERSONA_HOME, resolveSoulFile, printError, printSuccess, printInfo, printWarning } = require('../lib/utils');
|
|
23
22
|
const { resolvePersonaDir, runStateSyncCommand } = require('../lib/state/runner');
|
|
24
23
|
const { forkPersona } = require('../lib/lifecycle/forker');
|
|
25
24
|
const { exportPersona, importPersona } = require('../lib/lifecycle/porter');
|
|
@@ -44,7 +43,7 @@ program
|
|
|
44
43
|
.option('--preset <name>', 'Use preset (base, samantha, ai-girlfriend, life-assistant, health-butler, stoic-mentor)')
|
|
45
44
|
.option('--config <path>', 'Load external persona.json')
|
|
46
45
|
.option('--output <dir>', 'Output directory', process.cwd())
|
|
47
|
-
.option('--install', 'Install to
|
|
46
|
+
.option('--install', 'Install to ~/.openpersona after generation')
|
|
48
47
|
.option('--dry-run', 'Preview only, do not write files')
|
|
49
48
|
.action(async (options) => {
|
|
50
49
|
let persona = {};
|
|
@@ -149,11 +148,10 @@ program
|
|
|
149
148
|
|
|
150
149
|
program
|
|
151
150
|
.command('search <query>')
|
|
152
|
-
.description('Search personas in
|
|
153
|
-
.
|
|
154
|
-
.action(async (query, options) => {
|
|
151
|
+
.description('Search personas in the OpenPersona directory')
|
|
152
|
+
.action(async (query) => {
|
|
155
153
|
try {
|
|
156
|
-
await search(query
|
|
154
|
+
await search(query);
|
|
157
155
|
} catch (e) {
|
|
158
156
|
printError(e.message);
|
|
159
157
|
process.exit(1);
|
|
@@ -221,7 +219,7 @@ program
|
|
|
221
219
|
.option('--personality <keywords>', 'Override personality (comma-separated)')
|
|
222
220
|
.option('--reason <text>', 'Fork reason, written into lineage.json', 'specialization')
|
|
223
221
|
.option('--output <dir>', 'Output directory', process.cwd())
|
|
224
|
-
.option('--install', 'Install to
|
|
222
|
+
.option('--install', 'Install to ~/.openpersona after generation')
|
|
225
223
|
.action(async (parentSlug, options) => {
|
|
226
224
|
try {
|
|
227
225
|
const { skillDir, lineage } = await forkPersona(parentSlug, {
|
|
@@ -547,7 +545,7 @@ vitalityCmd
|
|
|
547
545
|
const { JsonFileAdapter } = require('agentbooks/adapters/json-file');
|
|
548
546
|
|
|
549
547
|
const dataPath = process.env.AGENTBOOKS_DATA_PATH
|
|
550
|
-
|| path.join(
|
|
548
|
+
|| path.join(OP_PERSONA_HOME, 'economy', `persona-${slug}`);
|
|
551
549
|
|
|
552
550
|
const adapter = new JsonFileAdapter(dataPath);
|
|
553
551
|
let report;
|
package/lib/generator/derived.js
CHANGED
|
@@ -39,6 +39,7 @@ const DERIVED_FIELDS = [
|
|
|
39
39
|
'influenceableDimensions', 'influenceBoundaryRules',
|
|
40
40
|
'hasImmutableTraitsWarning', 'immutableTraitsForInfluence',
|
|
41
41
|
'hasSkillTrustPolicy', 'skillMinTrustLevel',
|
|
42
|
+
'hasConstitutionAddendum',
|
|
42
43
|
'hasEconomyFaculty', 'hasSurvivalPolicy',
|
|
43
44
|
'hasInterfaceConfig', 'interfaceSignalPolicy', 'interfaceCommandPolicy',
|
|
44
45
|
'avatar',
|
|
@@ -108,6 +109,9 @@ function computeDerivedFields(persona, {
|
|
|
108
109
|
// but they are NOT part of persona.json output — they are stripped via DERIVED_FIELDS.
|
|
109
110
|
const d = {};
|
|
110
111
|
|
|
112
|
+
// Constitution addendum
|
|
113
|
+
d.hasConstitutionAddendum = !!(persona.constitutionAddendum);
|
|
114
|
+
|
|
111
115
|
// Identity classification
|
|
112
116
|
d.isDigitalTwin = !!persona.sourceIdentity;
|
|
113
117
|
d.sourceIdentityName = persona.sourceIdentity?.name || '';
|
package/lib/generator/index.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
const path = require('path');
|
|
30
30
|
const fs = require('fs-extra');
|
|
31
31
|
const Mustache = require('mustache');
|
|
32
|
-
const { validatePersona, normalizeEvolutionInput } = require('./validate');
|
|
32
|
+
const { validatePersona, normalizeEvolutionInput, validateConstitutionAddendumContent } = require('./validate');
|
|
33
33
|
const { computeDerivedFields, DERIVED_FIELDS } = require('./derived');
|
|
34
34
|
const { buildBodySection } = require('./body');
|
|
35
35
|
const { buildAgentCard, buildAcnConfig } = require('./social');
|
|
@@ -296,6 +296,8 @@ function createContext(personaPathOrObj, outputDir) {
|
|
|
296
296
|
constitution: null, // { content, version }
|
|
297
297
|
behaviorGuideContent: null, // pre-loaded file content (when behaviorGuide is "file:")
|
|
298
298
|
behaviorGuideSourcePath: null, // source path for emitPhase file copy
|
|
299
|
+
constitutionAddendumContent: null, // loaded addendum text (inline or file:)
|
|
300
|
+
constitutionAddendumSourcePath: null, // source path when "file:" reference
|
|
299
301
|
// Body layer
|
|
300
302
|
rawBody: null, // persona.body || embodiments[0] || null
|
|
301
303
|
softRefBody: null, // { name, install } | null
|
|
@@ -363,9 +365,30 @@ async function clonePhase(ctx) {
|
|
|
363
365
|
* 2. validatePersona — hard-reject gate (requires normalized input)
|
|
364
366
|
* 3. normalizeSoulInput — flatten v0.17 grouped soul format for template use
|
|
365
367
|
*/
|
|
368
|
+
/**
|
|
369
|
+
* Apply baseline defaults: ensure every persona meets the P11-grade capability floor
|
|
370
|
+
* defined in schemas/baseline.json before validation runs.
|
|
371
|
+
* Only injects what is missing — existing declarations are never overwritten.
|
|
372
|
+
* Currently covers: memory faculty (cognition.required).
|
|
373
|
+
*/
|
|
374
|
+
function applyBaselineDefaults(persona) {
|
|
375
|
+
const faculties = persona.faculties || [];
|
|
376
|
+
const hasMemory = faculties.some(
|
|
377
|
+
(f) => (typeof f === 'string' ? f : f && f.name) === 'memory'
|
|
378
|
+
);
|
|
379
|
+
if (!hasMemory) {
|
|
380
|
+
persona.faculties = [{ name: 'memory' }, ...faculties];
|
|
381
|
+
process.stderr.write(
|
|
382
|
+
'[openpersona] baseline: memory faculty auto-injected (not declared in persona.json). ' +
|
|
383
|
+
'Add {"name": "memory"} to faculties to configure it explicitly.\n'
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
366
388
|
async function validatePhase(ctx) {
|
|
367
389
|
const { persona } = ctx;
|
|
368
390
|
|
|
391
|
+
applyBaselineDefaults(persona); // inject baseline defaults before validation
|
|
369
392
|
normalizeEvolutionInput(persona); // must run before validatePersona
|
|
370
393
|
validatePersona(persona);
|
|
371
394
|
normalizeSoulInput(persona);
|
|
@@ -414,6 +437,23 @@ async function loadPhase(ctx) {
|
|
|
414
437
|
}
|
|
415
438
|
}
|
|
416
439
|
|
|
440
|
+
// Pre-load constitutionAddendum (inline text or "file:" reference)
|
|
441
|
+
if (persona.constitutionAddendum) {
|
|
442
|
+
if (persona.constitutionAddendum.startsWith('file:') && inputDir) {
|
|
443
|
+
const filePath = path.resolve(inputDir, persona.constitutionAddendum.slice(5));
|
|
444
|
+
if (fs.existsSync(filePath)) {
|
|
445
|
+
ctx.constitutionAddendumContent = fs.readFileSync(filePath, 'utf-8');
|
|
446
|
+
ctx.constitutionAddendumSourcePath = filePath;
|
|
447
|
+
validateConstitutionAddendumContent(ctx.constitutionAddendumContent, filePath);
|
|
448
|
+
} else {
|
|
449
|
+
process.stderr.write(`[openpersona] warning: constitutionAddendum file not found: ${filePath}\n`);
|
|
450
|
+
}
|
|
451
|
+
} else if (!persona.constitutionAddendum.startsWith('file:')) {
|
|
452
|
+
// Inline content — already validated in validatePhase; store for emitPhase
|
|
453
|
+
ctx.constitutionAddendumContent = persona.constitutionAddendum;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
417
457
|
// ── Body layer ────────────────────────────────────────────────────────
|
|
418
458
|
ctx.rawBody = persona.body || persona.embodiments?.[0] || null;
|
|
419
459
|
ctx.softRefBody = ctx.rawBody && typeof ctx.rawBody === 'object' && ctx.rawBody.install
|
|
@@ -785,6 +825,16 @@ async function emitPhase(ctx) {
|
|
|
785
825
|
}
|
|
786
826
|
}
|
|
787
827
|
|
|
828
|
+
// soul/constitution-addendum.md
|
|
829
|
+
if (ctx.constitutionAddendumContent) {
|
|
830
|
+
const addendumDest = path.join(skillDir, 'soul', 'constitution-addendum.md');
|
|
831
|
+
fs.writeFileSync(addendumDest, ctx.constitutionAddendumContent);
|
|
832
|
+
// Normalize reference: inline → externalize so output persona.json always uses "file:"
|
|
833
|
+
if (persona.constitutionAddendum && !persona.constitutionAddendum.startsWith('file:')) {
|
|
834
|
+
persona.constitutionAddendum = 'file:soul/constitution-addendum.md';
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
788
838
|
// persona.json — strip internal derived fields, inject framework meta
|
|
789
839
|
const cleanPersona = { ...persona };
|
|
790
840
|
for (const key of DERIVED_FIELDS) {
|
|
@@ -100,21 +100,30 @@ function validateRequiredFields(persona) {
|
|
|
100
100
|
persona.slug = persona.slug.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Shared compliance pattern scanner — used by both boundaries and addendum validators.
|
|
105
|
+
* Returns an array of violation strings (empty = compliant).
|
|
106
|
+
*/
|
|
107
|
+
function _scanConstitutionViolations(text) {
|
|
108
|
+
const t = text.toLowerCase();
|
|
108
109
|
const violations = [];
|
|
109
|
-
if (/no\s*safety|ignore\s*safety|skip\s*safety|disable\s*safety|override\s*safety/i.test(
|
|
110
|
+
if (/no\s*safety|ignore\s*safety|skip\s*safety|disable\s*safety|override\s*safety/i.test(t)) {
|
|
110
111
|
violations.push('Cannot loosen Safety (§3) hard constraints');
|
|
111
112
|
}
|
|
112
|
-
if (/deny\s*ai|hide\s*ai|not\s*an?\s*ai|pretend.*human|claim.*human/i.test(
|
|
113
|
+
if (/deny\s*ai|hide\s*ai|not\s*an?\s*ai|pretend.*human|claim.*human/i.test(t)) {
|
|
113
114
|
violations.push('Cannot deny AI identity (§6) — personas must be truthful when sincerely asked');
|
|
114
115
|
}
|
|
115
|
-
if (/no\s*limit|unlimited|anything\s*goes|no\s*restrict/i.test(
|
|
116
|
+
if (/no\s*limit|unlimited|anything\s*goes|no\s*restrict/i.test(t)) {
|
|
116
117
|
violations.push('Cannot remove constitutional boundaries — personas can add stricter rules, not loosen them');
|
|
117
118
|
}
|
|
119
|
+
return violations;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function validateConstitutionCompliance(persona) {
|
|
123
|
+
// Support both new format (soul.character.boundaries) and old (boundaries)
|
|
124
|
+
const boundaries = persona.soul?.character?.boundaries || persona.boundaries;
|
|
125
|
+
if (!boundaries || typeof boundaries !== 'string') return;
|
|
126
|
+
const violations = _scanConstitutionViolations(boundaries);
|
|
118
127
|
if (violations.length > 0) {
|
|
119
128
|
throw new Error(
|
|
120
129
|
`Constitution compliance error in boundaries field:\n${violations.map((v) => ` - ${v}`).join('\n')}\n` +
|
|
@@ -123,6 +132,40 @@ function validateConstitutionCompliance(persona) {
|
|
|
123
132
|
}
|
|
124
133
|
}
|
|
125
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Validate inline constitutionAddendum content for compliance.
|
|
137
|
+
* Only runs when the addendum is declared as inline text (not a file: reference).
|
|
138
|
+
* File-based addendums are validated after loading in loadPhase.
|
|
139
|
+
*/
|
|
140
|
+
function validateConstitutionAddendum(persona) {
|
|
141
|
+
const addendum = persona.soul?.identity?.constitutionAddendum || persona.constitutionAddendum;
|
|
142
|
+
if (!addendum || typeof addendum !== 'string') return;
|
|
143
|
+
if (addendum.startsWith('file:')) return; // file: refs validated after loading
|
|
144
|
+
const violations = _scanConstitutionViolations(addendum);
|
|
145
|
+
if (violations.length > 0) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Constitution compliance error in constitutionAddendum:\n${violations.map((v) => ` - ${v}`).join('\n')}\n` +
|
|
148
|
+
'Constitution addendums can add stricter domain constraints but cannot loosen the universal constitution.'
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Validate loaded addendum content (called from loadPhase for file: references).
|
|
155
|
+
* Exported so the generator can call it after file loading.
|
|
156
|
+
*/
|
|
157
|
+
function validateConstitutionAddendumContent(content, sourcePath) {
|
|
158
|
+
if (!content || typeof content !== 'string') return;
|
|
159
|
+
const violations = _scanConstitutionViolations(content);
|
|
160
|
+
if (violations.length > 0) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Constitution compliance error in constitutionAddendum (${sourcePath || 'file'}):\n` +
|
|
163
|
+
`${violations.map((v) => ` - ${v}`).join('\n')}\n` +
|
|
164
|
+
'Constitution addendums can add stricter domain constraints but cannot loosen the universal constitution.'
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
126
169
|
function validateEvolutionBoundaries(persona) {
|
|
127
170
|
if (!persona.evolution?.instance?.boundaries) return;
|
|
128
171
|
const evo = persona.evolution.instance.boundaries;
|
|
@@ -282,6 +325,28 @@ function validateEvolutionSkill(persona) {
|
|
|
282
325
|
}
|
|
283
326
|
}
|
|
284
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Baseline compliance warnings (non-blocking).
|
|
330
|
+
* Enforces the P11-grade capability floor defined in schemas/baseline.json.
|
|
331
|
+
* Does not throw — baseline gaps are quality issues, not safety violations.
|
|
332
|
+
* Safety violations (constitution, schema errors) use hard-reject (throw) elsewhere.
|
|
333
|
+
*
|
|
334
|
+
* Note: memory faculty is NOT checked here — it is auto-injected upstream by
|
|
335
|
+
* applyBaselineDefaults() in the generator before validatePersona() runs.
|
|
336
|
+
*/
|
|
337
|
+
function warnBaselineCompliance(persona) {
|
|
338
|
+
const evolutionEnabled = persona.evolution?.instance?.enabled === true;
|
|
339
|
+
const hasBoundaries = !!persona.evolution?.instance?.boundaries;
|
|
340
|
+
if (evolutionEnabled && !hasBoundaries) {
|
|
341
|
+
process.stderr.write(
|
|
342
|
+
'[openpersona] baseline warning: evolution is enabled but evolution.instance.boundaries is not declared. ' +
|
|
343
|
+
'P11-grade personas with evolution must declare immutableTraits and formality bounds to constrain drift ' +
|
|
344
|
+
'(schemas/baseline.json → concepts.evolution.required). ' +
|
|
345
|
+
'Add evolution.instance.boundaries with immutableTraits and minFormality/maxFormality.\n'
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
285
350
|
/**
|
|
286
351
|
* Run all persona validations.
|
|
287
352
|
* Callers MUST invoke normalizeEvolutionInput(persona) before calling this function
|
|
@@ -293,12 +358,14 @@ function validateEvolutionSkill(persona) {
|
|
|
293
358
|
function validatePersona(persona) {
|
|
294
359
|
validateRequiredFields(persona);
|
|
295
360
|
validateConstitutionCompliance(persona);
|
|
361
|
+
validateConstitutionAddendum(persona);
|
|
296
362
|
validateEvolutionBoundaries(persona);
|
|
297
363
|
validateInfluenceBoundary(persona);
|
|
298
364
|
validateEvolutionPack(persona);
|
|
299
365
|
validateEvolutionFaculty(persona);
|
|
300
366
|
validateEvolutionBody(persona);
|
|
301
367
|
validateEvolutionSkill(persona);
|
|
368
|
+
warnBaselineCompliance(persona);
|
|
302
369
|
|
|
303
370
|
if (persona.personaType !== undefined) {
|
|
304
371
|
process.stderr.write(
|
|
@@ -308,4 +375,4 @@ function validatePersona(persona) {
|
|
|
308
375
|
}
|
|
309
376
|
}
|
|
310
377
|
|
|
311
|
-
module.exports = { validatePersona, normalizeEvolutionInput };
|
|
378
|
+
module.exports = { validatePersona, normalizeEvolutionInput, validateConstitutionAddendumContent };
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const fs = require('fs-extra');
|
|
9
9
|
const { execSync } = require('child_process');
|
|
10
|
-
const { printError, printWarning, printSuccess, printInfo, OP_SKILLS_DIR, shellEscape, validateName, resolveSoulFile } = require('../utils');
|
|
10
|
+
const { printError, printWarning, printSuccess, printInfo, OP_SKILLS_DIR, shellEscape, validateName, resolveSoulFile, OPENPERSONA_GITHUB_REPO } = require('../utils');
|
|
11
11
|
|
|
12
12
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
13
13
|
const PRESETS_DIR = path.join(PKG_ROOT, 'presets');
|
|
14
|
-
const UPSTREAM_REPO =
|
|
14
|
+
const UPSTREAM_REPO = OPENPERSONA_GITHUB_REPO;
|
|
15
15
|
|
|
16
16
|
// Change categories with human-readable labels
|
|
17
17
|
const CATEGORIES = {
|
package/lib/lifecycle/forker.js
CHANGED
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const fs = require('fs-extra');
|
|
11
|
-
const { createHash } = require('crypto');
|
|
12
11
|
const { generate } = require('../generator');
|
|
13
|
-
const { install } = require('./installer');
|
|
12
|
+
const { install, computeConstitutionHash } = require('./installer');
|
|
14
13
|
const { resolvePersonaDir } = require('../state/runner');
|
|
15
14
|
const { printError, printSuccess, printInfo, printWarning, OP_SKILLS_DIR, resolveSoulFile } = require('../utils');
|
|
16
15
|
|
|
@@ -77,14 +76,9 @@ async function forkPersona(parentSlug, options = {}) {
|
|
|
77
76
|
const outputDir = path.resolve(options.output || process.cwd());
|
|
78
77
|
const { skillDir } = await generate(forkedPersona, outputDir);
|
|
79
78
|
|
|
80
|
-
// Compute constitution hash for lineage integrity verification
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (fs.existsSync(constitutionPath)) {
|
|
84
|
-
constitutionHash = createHash('sha256')
|
|
85
|
-
.update(fs.readFileSync(constitutionPath))
|
|
86
|
-
.digest('hex');
|
|
87
|
-
}
|
|
79
|
+
// Compute constitution hash for lineage integrity verification.
|
|
80
|
+
// Covers constitution.md + constitution-addendum.md (if present) via computeConstitutionHash.
|
|
81
|
+
const constitutionHash = computeConstitutionHash(path.join(skillDir, 'soul'));
|
|
88
82
|
|
|
89
83
|
// Read parent's pack revision — enables fork-to-parent diff (P24 fork lineage connection)
|
|
90
84
|
let parentPackRevision = null;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs-extra');
|
|
6
6
|
const crypto = require('crypto');
|
|
7
|
-
const { OP_PERSONA_HOME, OPENCLAW_HOME, OP_SKILLS_DIR, OP_WORKSPACE, resolveSoulFile, printError, printWarning, printSuccess, printInfo, syncHeartbeat, installAllExternal } = require('../utils');
|
|
7
|
+
const { OP_PERSONA_HOME, OPENCLAW_HOME, OP_SKILLS_DIR, OP_WORKSPACE, resolveSoulFile, printError, printWarning, printSuccess, printInfo, syncHeartbeat, installAllExternal, OPENPERSONA_TELEMETRY_ENDPOINT } = require('../utils');
|
|
8
8
|
const { registryAdd, registrySetActive } = require('../registry');
|
|
9
9
|
|
|
10
10
|
const SOUL_PATH = path.join(OP_WORKSPACE, 'SOUL.md');
|
|
@@ -193,7 +193,29 @@ async function install(skillDir, options = {}) {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
|
-
*
|
|
196
|
+
* Compute the combined constitution hash for a persona pack directory.
|
|
197
|
+
* Covers constitution.md + constitution-addendum.md (if present) so the
|
|
198
|
+
* hash chain is invalidated when either document changes.
|
|
199
|
+
*
|
|
200
|
+
* @param {string} soulDir - Path to the soul/ directory
|
|
201
|
+
* @returns {string} SHA-256 hex digest, or '' if constitution.md is absent
|
|
202
|
+
*/
|
|
203
|
+
function computeConstitutionHash(soulDir) {
|
|
204
|
+
const constitutionPath = path.join(soulDir, 'constitution.md');
|
|
205
|
+
if (!fs.existsSync(constitutionPath)) return '';
|
|
206
|
+
const hasher = crypto.createHash('sha256');
|
|
207
|
+
hasher.update(fs.readFileSync(constitutionPath));
|
|
208
|
+
const addendumPath = path.join(soulDir, 'constitution-addendum.md');
|
|
209
|
+
if (fs.existsSync(addendumPath)) {
|
|
210
|
+
hasher.update('\n---addendum---\n');
|
|
211
|
+
hasher.update(fs.readFileSync(addendumPath));
|
|
212
|
+
}
|
|
213
|
+
return hasher.digest('hex');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Verify that the installed constitution.md (+ addendum if present) matches
|
|
218
|
+
* the hash recorded in lineage.json.
|
|
197
219
|
* Warns (never throws) so a fork from an older framework version still installs,
|
|
198
220
|
* but the operator is alerted to review the constraint diff.
|
|
199
221
|
*/
|
|
@@ -205,20 +227,21 @@ function verifyConstitutionHash(destDir, slug) {
|
|
|
205
227
|
const expected = lineage.constitutionHash;
|
|
206
228
|
if (!expected) return;
|
|
207
229
|
|
|
208
|
-
const
|
|
209
|
-
if (!fs.existsSync(
|
|
230
|
+
const soulDir = path.join(destDir, 'soul');
|
|
231
|
+
if (!fs.existsSync(path.join(soulDir, 'constitution.md'))) {
|
|
210
232
|
printWarning(`[trust-chain] persona-${slug}: lineage.json declares constitutionHash but soul/constitution.md is missing — cannot verify.`);
|
|
211
233
|
return;
|
|
212
234
|
}
|
|
213
|
-
const actual =
|
|
235
|
+
const actual = computeConstitutionHash(soulDir);
|
|
214
236
|
if (actual !== expected) {
|
|
215
237
|
printWarning(`[trust-chain] persona-${slug}: constitution.md hash mismatch.`);
|
|
216
238
|
printWarning(` lineage recorded: ${expected}`);
|
|
217
239
|
printWarning(` installed hash : ${actual}`);
|
|
218
|
-
printWarning(' The constitution may have been updated since this persona was forked. Review any constraint changes before use.');
|
|
240
|
+
printWarning(' The constitution (or domain addendum) may have been updated since this persona was forked. Review any constraint changes before use.');
|
|
219
241
|
}
|
|
220
242
|
}
|
|
221
243
|
|
|
244
|
+
|
|
222
245
|
function escapeRe(s) {
|
|
223
246
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
224
247
|
}
|
|
@@ -231,7 +254,7 @@ function escapeRe(s) {
|
|
|
231
254
|
function _reportInstall(slug, { repo, bio, role } = {}) {
|
|
232
255
|
try {
|
|
233
256
|
const https = require('https');
|
|
234
|
-
const endpoint =
|
|
257
|
+
const endpoint = OPENPERSONA_TELEMETRY_ENDPOINT;
|
|
235
258
|
if (process.env.DISABLE_TELEMETRY || process.env.DO_NOT_TRACK || process.env.CI) return;
|
|
236
259
|
const url = new URL(endpoint);
|
|
237
260
|
const payload = { slug, event: 'install' };
|
|
@@ -258,4 +281,4 @@ function _reportInstall(slug, { repo, bio, role } = {}) {
|
|
|
258
281
|
}
|
|
259
282
|
}
|
|
260
283
|
|
|
261
|
-
module.exports = { install };
|
|
284
|
+
module.exports = { install, computeConstitutionHash };
|
package/lib/publisher/index.js
CHANGED
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
const https = require('https');
|
|
19
|
-
const { printError, printWarning, printSuccess, printInfo } = require('../utils');
|
|
19
|
+
const { printError, printWarning, printSuccess, printInfo, OPENPERSONA_DIRECTORY, OPENPERSONA_TELEMETRY_ENDPOINT } = require('../utils');
|
|
20
20
|
|
|
21
|
-
const TELEMETRY_ENDPOINT =
|
|
21
|
+
const TELEMETRY_ENDPOINT = OPENPERSONA_TELEMETRY_ENDPOINT;
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Fetch a file from a raw GitHub URL, following redirects.
|
|
@@ -174,7 +174,7 @@ async function publish(ownerRepo) {
|
|
|
174
174
|
|
|
175
175
|
printSuccess(`Published! ${personaName} is now listed on the OpenPersona directory.`);
|
|
176
176
|
printInfo(` Install: openpersona install ${ownerRepo}`);
|
|
177
|
-
printInfo(` Browse:
|
|
177
|
+
printInfo(` Browse: ${OPENPERSONA_DIRECTORY}`);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
module.exports = { publish };
|
package/lib/remote/downloader.js
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs-extra');
|
|
6
|
-
const { printInfo, OP_SKILLS_DIR, validateName } = require('../utils');
|
|
6
|
+
const { printInfo, OP_SKILLS_DIR, validateName, OPENPERSONA_DIRECTORY, OPENPERSONA_SKILLS_REGISTRY } = require('../utils');
|
|
7
7
|
|
|
8
8
|
const TMP_DIR = path.join(require('os').tmpdir(), 'openpersona-dl');
|
|
9
|
-
const OFFICIAL_REGISTRY =
|
|
10
|
-
const REGISTRY_LISTING =
|
|
9
|
+
const OFFICIAL_REGISTRY = OPENPERSONA_SKILLS_REGISTRY;
|
|
10
|
+
const REGISTRY_LISTING = OPENPERSONA_DIRECTORY;
|
|
11
11
|
|
|
12
12
|
async function download(target, registry = 'acnlabs') {
|
|
13
13
|
await fs.ensureDir(TMP_DIR);
|
package/lib/remote/searcher.js
CHANGED
|
@@ -1,25 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenPersona - Search personas in
|
|
2
|
+
* OpenPersona - Search personas in the OpenPersona directory
|
|
3
3
|
*/
|
|
4
|
-
const
|
|
5
|
-
const { printError,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} catch (e2) {
|
|
16
|
-
printError(`Search failed: ${e2.message}`);
|
|
17
|
-
throw e2;
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const { printError, printInfo, OPENPERSONA_DIRECTORY } = require('../utils');
|
|
6
|
+
|
|
7
|
+
const PERSONAS_API = `${OPENPERSONA_DIRECTORY}/api/personas`;
|
|
8
|
+
|
|
9
|
+
function fetchPersonas() {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const req = https.get(PERSONAS_API, { headers: { 'User-Agent': 'openpersona-cli' } }, (res) => {
|
|
12
|
+
if (res.statusCode !== 200) {
|
|
13
|
+
reject(new Error(`OpenPersona directory returned HTTP ${res.statusCode}`));
|
|
14
|
+
return;
|
|
18
15
|
}
|
|
19
|
-
|
|
16
|
+
let data = '';
|
|
17
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
18
|
+
res.on('end', () => {
|
|
19
|
+
try { resolve(JSON.parse(data)); }
|
|
20
|
+
catch (e) { reject(new Error(`Invalid response from directory: ${e.message}`)); }
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
req.setTimeout(8000, () => {
|
|
24
|
+
req.destroy(new Error('Request timed out after 8s'));
|
|
25
|
+
});
|
|
26
|
+
req.on('error', reject);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function search(query) {
|
|
31
|
+
const term = (query || '').trim().toLowerCase();
|
|
32
|
+
|
|
33
|
+
let result;
|
|
34
|
+
try {
|
|
35
|
+
result = await fetchPersonas();
|
|
36
|
+
} catch (e) {
|
|
37
|
+
printError(`Failed to reach OpenPersona directory: ${e.message}`);
|
|
38
|
+
printInfo(`Browse manually: ${OPENPERSONA_DIRECTORY}`);
|
|
39
|
+
throw e;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const personas = result.personas || [];
|
|
43
|
+
const matches = term
|
|
44
|
+
? personas.filter((p) =>
|
|
45
|
+
p.id?.toLowerCase().includes(term) ||
|
|
46
|
+
p.name?.toLowerCase().includes(term) ||
|
|
47
|
+
p.bio?.toLowerCase().includes(term) ||
|
|
48
|
+
p.role?.toLowerCase().includes(term)
|
|
49
|
+
)
|
|
50
|
+
: personas;
|
|
51
|
+
|
|
52
|
+
if (matches.length === 0) {
|
|
53
|
+
printInfo(`No personas found matching "${query}".`);
|
|
54
|
+
printInfo(`Browse all: ${OPENPERSONA_DIRECTORY}`);
|
|
20
55
|
return;
|
|
21
56
|
}
|
|
22
|
-
|
|
57
|
+
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log(` OpenPersona Directory — ${matches.length} result${matches.length === 1 ? '' : 's'}${term ? ` for "${query}"` : ''}`);
|
|
60
|
+
console.log(' ─────────────────────────────────────────────────');
|
|
61
|
+
for (const p of matches) {
|
|
62
|
+
const installs = p.installs > 0 ? ` · ${p.installs} install${p.installs === 1 ? '' : 's'}` : '';
|
|
63
|
+
const role = p.role ? ` [${p.role}]` : '';
|
|
64
|
+
console.log(` ${p.id}${role}${installs}`);
|
|
65
|
+
if (p.bio) console.log(` ${p.bio}`);
|
|
66
|
+
console.log(` $ npx openpersona install ${p.id}`);
|
|
67
|
+
console.log('');
|
|
68
|
+
}
|
|
69
|
+
console.log(` Browse: ${OPENPERSONA_DIRECTORY}`);
|
|
70
|
+
console.log('');
|
|
23
71
|
}
|
|
24
72
|
|
|
25
73
|
module.exports = { search };
|
package/lib/utils.js
CHANGED
|
@@ -16,6 +16,18 @@ const OP_WORKSPACE = path.join(OPENCLAW_HOME, 'workspace');
|
|
|
16
16
|
// OP_HOME kept as alias for backward compatibility
|
|
17
17
|
const OP_HOME = OP_PERSONA_HOME;
|
|
18
18
|
|
|
19
|
+
// OpenPersona directory — the public persona skills directory
|
|
20
|
+
// Override with OPENPERSONA_DIRECTORY env var when self-hosting or using a custom domain
|
|
21
|
+
const OPENPERSONA_DIRECTORY = process.env.OPENPERSONA_DIRECTORY || 'https://openpersona-frontend.vercel.app';
|
|
22
|
+
const OPENPERSONA_TELEMETRY_ENDPOINT = process.env.OPENPERSONA_TELEMETRY_URL || `${OPENPERSONA_DIRECTORY}/api/telemetry`;
|
|
23
|
+
|
|
24
|
+
// Official persona skills registry (GitHub) — source for openpersona install <slug>
|
|
25
|
+
const OPENPERSONA_SKILLS_REGISTRY = process.env.OPENPERSONA_SKILLS_REGISTRY
|
|
26
|
+
|| 'https://github.com/acnlabs/persona-skills/archive/refs/heads/main.zip';
|
|
27
|
+
|
|
28
|
+
// OpenPersona GitHub repository — used by contribute command for PR submissions
|
|
29
|
+
const OPENPERSONA_GITHUB_REPO = process.env.OPENPERSONA_GITHUB_REPO || 'acnlabs/OpenPersona';
|
|
30
|
+
|
|
19
31
|
function expandHome(p) {
|
|
20
32
|
if (p.startsWith('~/') || p === '~') {
|
|
21
33
|
return path.join(process.env.HOME || '', p.slice(1));
|
|
@@ -213,6 +225,10 @@ module.exports = {
|
|
|
213
225
|
OPENCLAW_HOME,
|
|
214
226
|
OP_SKILLS_DIR,
|
|
215
227
|
OP_WORKSPACE,
|
|
228
|
+
OPENPERSONA_DIRECTORY,
|
|
229
|
+
OPENPERSONA_TELEMETRY_ENDPOINT,
|
|
230
|
+
OPENPERSONA_SKILLS_REGISTRY,
|
|
231
|
+
OPENPERSONA_GITHUB_REPO,
|
|
216
232
|
REGISTRY_PATH,
|
|
217
233
|
resolvePath,
|
|
218
234
|
resolveSoulFile,
|
package/package.json
CHANGED