@personaxis/core 0.11.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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent.d.ts +105 -0
  3. package/dist/agent.js +429 -0
  4. package/dist/appraisal.d.ts +196 -0
  5. package/dist/appraisal.js +159 -0
  6. package/dist/approval.d.ts +42 -0
  7. package/dist/approval.js +71 -0
  8. package/dist/blackboard.d.ts +86 -0
  9. package/dist/blackboard.js +139 -0
  10. package/dist/compile/assemble.d.ts +44 -0
  11. package/dist/compile/assemble.js +338 -0
  12. package/dist/compile/dist.d.ts +30 -0
  13. package/dist/compile/dist.js +66 -0
  14. package/dist/compile/faithfulness.d.ts +47 -0
  15. package/dist/compile/faithfulness.js +112 -0
  16. package/dist/compile/index.d.ts +10 -0
  17. package/dist/compile/index.js +10 -0
  18. package/dist/compile/targets.d.ts +62 -0
  19. package/dist/compile/targets.js +114 -0
  20. package/dist/config-layers.d.ts +42 -0
  21. package/dist/config-layers.js +59 -0
  22. package/dist/config-scan.d.ts +30 -0
  23. package/dist/config-scan.js +118 -0
  24. package/dist/context.d.ts +67 -0
  25. package/dist/context.js +178 -0
  26. package/dist/envelopes.d.ts +48 -0
  27. package/dist/envelopes.js +131 -0
  28. package/dist/events.d.ts +135 -0
  29. package/dist/events.js +19 -0
  30. package/dist/evolution-view.d.ts +43 -0
  31. package/dist/evolution-view.js +67 -0
  32. package/dist/generated/version.d.ts +1 -0
  33. package/dist/generated/version.js +2 -0
  34. package/dist/governance.d.ts +108 -0
  35. package/dist/governance.js +204 -0
  36. package/dist/heuristic-appraiser.d.ts +13 -0
  37. package/dist/heuristic-appraiser.js +48 -0
  38. package/dist/hooks.d.ts +52 -0
  39. package/dist/hooks.js +122 -0
  40. package/dist/index.d.ts +53 -0
  41. package/dist/index.js +53 -0
  42. package/dist/injection.d.ts +48 -0
  43. package/dist/injection.js +141 -0
  44. package/dist/live-sync.d.ts +60 -0
  45. package/dist/live-sync.js +85 -0
  46. package/dist/llm-appraiser.d.ts +31 -0
  47. package/dist/llm-appraiser.js +143 -0
  48. package/dist/lock.d.ts +26 -0
  49. package/dist/lock.js +99 -0
  50. package/dist/loop.d.ts +52 -0
  51. package/dist/loop.js +253 -0
  52. package/dist/memory-kinds.d.ts +67 -0
  53. package/dist/memory-kinds.js +106 -0
  54. package/dist/memory.d.ts +123 -0
  55. package/dist/memory.js +228 -0
  56. package/dist/model-config.d.ts +60 -0
  57. package/dist/model-config.js +102 -0
  58. package/dist/persona-theme.d.ts +57 -0
  59. package/dist/persona-theme.js +183 -0
  60. package/dist/persona.d.ts +88 -0
  61. package/dist/persona.js +93 -0
  62. package/dist/ports/index.d.ts +81 -0
  63. package/dist/ports/index.js +50 -0
  64. package/dist/provenance.d.ts +63 -0
  65. package/dist/provenance.js +132 -0
  66. package/dist/recompile-marker.d.ts +19 -0
  67. package/dist/recompile-marker.js +36 -0
  68. package/dist/registry.d.ts +99 -0
  69. package/dist/registry.js +168 -0
  70. package/dist/responder.d.ts +47 -0
  71. package/dist/responder.js +78 -0
  72. package/dist/sandbox.d.ts +120 -0
  73. package/dist/sandbox.js +265 -0
  74. package/dist/self-evolution.d.ts +148 -0
  75. package/dist/self-evolution.js +364 -0
  76. package/dist/session-writer.d.ts +52 -0
  77. package/dist/session-writer.js +104 -0
  78. package/dist/sessions.d.ts +96 -0
  79. package/dist/sessions.js +179 -0
  80. package/dist/sigil.d.ts +41 -0
  81. package/dist/sigil.js +92 -0
  82. package/dist/skill-lifecycle.d.ts +74 -0
  83. package/dist/skill-lifecycle.js +126 -0
  84. package/dist/skill-review.d.ts +25 -0
  85. package/dist/skill-review.js +67 -0
  86. package/dist/spec-edit.d.ts +30 -0
  87. package/dist/spec-edit.js +137 -0
  88. package/dist/state-engine.d.ts +40 -0
  89. package/dist/state-engine.js +50 -0
  90. package/dist/state-rebuild.d.ts +48 -0
  91. package/dist/state-rebuild.js +60 -0
  92. package/dist/sync.d.ts +32 -0
  93. package/dist/sync.js +95 -0
  94. package/dist/tool-calling.d.ts +59 -0
  95. package/dist/tool-calling.js +168 -0
  96. package/dist/tool-repair.d.ts +20 -0
  97. package/dist/tool-repair.js +119 -0
  98. package/dist/tools/exec.d.ts +46 -0
  99. package/dist/tools/exec.js +132 -0
  100. package/dist/tools/registry.d.ts +38 -0
  101. package/dist/tools/registry.js +162 -0
  102. package/dist/trace.d.ts +63 -0
  103. package/dist/trace.js +162 -0
  104. package/dist/verification.d.ts +65 -0
  105. package/dist/verification.js +190 -0
  106. package/package.json +38 -0
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Blackboard multi-persona orchestration (F7 — plan/07-orchestration).
3
+ *
4
+ * The overseer doesn't hard-wire which persona does what. It posts a task to a
5
+ * shared blackboard; personas *volunteer* ranked by how well their declared
6
+ * capabilities match the task (Salemi et al., 2025 blackboard; the puppeteer
7
+ * orchestrator of Dang et al., 2025 picks among volunteers). This scales to a
8
+ * heterogeneous pool without a central registry of who-can-do-what.
9
+ *
10
+ * Everything is auditable: post → solicit (ranked) → assign → contribute → resolve,
11
+ * each an event on the board. The actual "work" is a pluggable worker (an LLM-backed
12
+ * loop in production; a stub in tests), so the orchestration is verifiable on its own.
13
+ */
14
+ const STOP = new Set(["the", "a", "an", "and", "or", "of", "to", "for", "in", "on", "with", "run", "the", "this", "that", "make", "do"]);
15
+ function tokenize(text) {
16
+ return text
17
+ .toLowerCase()
18
+ .replace(/[^a-z0-9\s/_-]/g, " ")
19
+ .split(/\s+/)
20
+ .filter((w) => w.length > 2 && !STOP.has(w));
21
+ }
22
+ /**
23
+ * Capability tokens for a persona. Prefers the explicit, machine-readable
24
+ * `identity.capabilities` (v0.8); falls back to deriving them from the identity
25
+ * layer (purpose / allowed_domains / role) when absent.
26
+ */
27
+ export function extractCapabilities(fm) {
28
+ const id = fm.identity;
29
+ // v0.8: explicit declaration wins (reliable routing, not heuristic).
30
+ if (Array.isArray(id?.capabilities)) {
31
+ const explicit = id.capabilities.filter((c) => typeof c === "string");
32
+ if (explicit.length > 0)
33
+ return [...new Set(explicit.flatMap((c) => tokenize(c)))];
34
+ }
35
+ const tokens = new Set();
36
+ const si = id?.system_identity;
37
+ if (si?.purpose)
38
+ tokenize(si.purpose).forEach((t) => tokens.add(t));
39
+ if (Array.isArray(si?.allowed_domains)) {
40
+ for (const d of si.allowed_domains)
41
+ if (typeof d === "string")
42
+ tokenize(d).forEach((t) => tokens.add(t));
43
+ }
44
+ const role = id?.role_identity;
45
+ if (typeof role === "string")
46
+ tokenize(role).forEach((t) => tokens.add(t));
47
+ else if (role?.primary_role)
48
+ tokenize(role.primary_role).forEach((t) => tokens.add(t));
49
+ return [...tokens];
50
+ }
51
+ /** Score how well an agent matches a task (Jaccard-like, recall-weighted). */
52
+ export function matchScore(taskTokens, capabilities) {
53
+ if (taskTokens.length === 0)
54
+ return { score: 0, matched: [] };
55
+ const caps = new Set(capabilities);
56
+ const matched = taskTokens.filter((t) => caps.has(t));
57
+ // recall over the task's needs, lightly boosted by capability breadth coverage
58
+ const score = matched.length / taskTokens.length;
59
+ return { score: Number(score.toFixed(3)), matched };
60
+ }
61
+ export class Blackboard {
62
+ tasks = new Map();
63
+ log = [];
64
+ seq = 0;
65
+ post(description) {
66
+ const id = `t${++this.seq}`;
67
+ const task = { id, description, tokens: tokenize(description), status: "open", contributions: [] };
68
+ this.tasks.set(id, task);
69
+ this.event("post", id, description);
70
+ return task;
71
+ }
72
+ /** Rank the agents who could take this task (highest score first, score>0). */
73
+ solicit(taskId, agents) {
74
+ const task = this.require(taskId);
75
+ return agents
76
+ .map((a) => {
77
+ const { score, matched } = matchScore(task.tokens, a.capabilities);
78
+ return { id: a.id, score, matched };
79
+ })
80
+ .filter((v) => v.score > 0)
81
+ .sort((a, b) => b.score - a.score);
82
+ }
83
+ /** Assign to a specific agent, or auto-assign the top volunteer. */
84
+ assign(taskId, agents, agentId) {
85
+ const task = this.require(taskId);
86
+ const ranked = this.solicit(taskId, agents);
87
+ const chosen = agentId ? ranked.find((v) => v.id === agentId) : ranked[0];
88
+ if (!chosen)
89
+ return null;
90
+ task.status = "assigned";
91
+ task.assignedTo = chosen.id;
92
+ this.event("assign", taskId, `${chosen.id} (score ${chosen.score})`);
93
+ return chosen;
94
+ }
95
+ contribute(taskId, agentId, content) {
96
+ const task = this.require(taskId);
97
+ task.contributions.push({ agentId, content, ts: new Date().toISOString() });
98
+ this.event("contribute", taskId, `${agentId}: ${content.slice(0, 60)}`);
99
+ }
100
+ resolve(taskId, result) {
101
+ const task = this.require(taskId);
102
+ task.status = "resolved";
103
+ task.result = result;
104
+ this.event("resolve", taskId, result.slice(0, 60));
105
+ }
106
+ get(taskId) {
107
+ return this.tasks.get(taskId);
108
+ }
109
+ all() {
110
+ return [...this.tasks.values()];
111
+ }
112
+ require(taskId) {
113
+ const t = this.tasks.get(taskId);
114
+ if (!t)
115
+ throw new Error(`no task ${taskId}`);
116
+ return t;
117
+ }
118
+ event(kind, taskId, detail) {
119
+ this.log.push({ ts: new Date().toISOString(), kind, taskId, detail });
120
+ }
121
+ }
122
+ /**
123
+ * Run one full blackboard cycle for a task over a pool of agents (puppeteer-style:
124
+ * solicit volunteers, assign the best, have it contribute, resolve).
125
+ */
126
+ export async function orchestrate(board, description, agents, opts = {}) {
127
+ const task = board.post(description);
128
+ const volunteers = board.solicit(task.id, agents);
129
+ const assigned = board.assign(task.id, agents);
130
+ if (!assigned)
131
+ return { task: board.get(task.id), volunteers, assigned: null };
132
+ const worker = opts.worker ??
133
+ ((a, t) => `${a.id} handled "${t.description}" (matched: ${matchScore(t.tokens, a.capabilities).matched.join(", ")})`);
134
+ const agent = agents.find((a) => a.id === assigned.id);
135
+ const contribution = await worker(agent, task);
136
+ board.contribute(task.id, assigned.id, contribution);
137
+ board.resolve(task.id, contribution);
138
+ return { task: board.get(task.id), volunteers, assigned, contribution };
139
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * F3.1 — the DETERMINISTIC compile assembler (stage 1 of the two-stage pipeline).
3
+ *
4
+ * This is the canonical, LLM-free path from a parsed persona spec to the
5
+ * compiled PERSONA-prompting document. It ALWAYS runs; it is:
6
+ * - what gets hashed (stable provenance — same spec ⇒ same bytes);
7
+ * - what the Living Loop writes on an inline recompile (cheap, no provider);
8
+ * - the fallback when no model provider is configured;
9
+ * - the ground-truth artifact the faithfulness check diffs a polished
10
+ * document against (see faithfulness.ts).
11
+ *
12
+ * It follows the section contract of PERSONA_template.md and writes the whole
13
+ * document in the SECOND PERSON. It NEVER emits runtime numbers (trait/affect
14
+ * tables, sigil seeds, a live-state block) — state lives in state.json.
15
+ *
16
+ * Field sourcing prefers the v1.0 layer-10 `persona` prompting fields (address,
17
+ * voice_exemplars, scene_contracts, behavioral_anchors, consistency) and
18
+ * degrades to the legacy top-level `persona_prompting` block, then to deriving
19
+ * the section from the quantitative layers. It invents nothing.
20
+ */
21
+ type Dict = Record<string, unknown>;
22
+ export interface AssembleTarget {
23
+ /** The name the document addresses ("You are <name>"). */
24
+ name: string;
25
+ isSubagent: boolean;
26
+ slug?: string;
27
+ /** Resource path prefix: "./" for a sub-persona, "./.personaxis/" for the root. */
28
+ resourceBase: string;
29
+ }
30
+ export interface AssembleInput {
31
+ /** Parsed persona frontmatter (v1.0 or legacy 0.x). */
32
+ persona: Dict;
33
+ /** The resource-manifest bullet list, reproduced verbatim. */
34
+ resourceManifest?: string;
35
+ target: AssembleTarget;
36
+ /** Applied governed self-edits (dot-path → value); authoritative over the spec. */
37
+ appliedOverlay?: Record<string, unknown>;
38
+ }
39
+ /**
40
+ * Assemble the canonical compiled persona document. Deterministic: the same
41
+ * persona spec (+ manifest + target) always produces byte-identical output.
42
+ */
43
+ export declare function assemblePersonaDoc(input: AssembleInput): string;
44
+ export {};
@@ -0,0 +1,338 @@
1
+ /**
2
+ * F3.1 — the DETERMINISTIC compile assembler (stage 1 of the two-stage pipeline).
3
+ *
4
+ * This is the canonical, LLM-free path from a parsed persona spec to the
5
+ * compiled PERSONA-prompting document. It ALWAYS runs; it is:
6
+ * - what gets hashed (stable provenance — same spec ⇒ same bytes);
7
+ * - what the Living Loop writes on an inline recompile (cheap, no provider);
8
+ * - the fallback when no model provider is configured;
9
+ * - the ground-truth artifact the faithfulness check diffs a polished
10
+ * document against (see faithfulness.ts).
11
+ *
12
+ * It follows the section contract of PERSONA_template.md and writes the whole
13
+ * document in the SECOND PERSON. It NEVER emits runtime numbers (trait/affect
14
+ * tables, sigil seeds, a live-state block) — state lives in state.json.
15
+ *
16
+ * Field sourcing prefers the v1.0 layer-10 `persona` prompting fields (address,
17
+ * voice_exemplars, scene_contracts, behavioral_anchors, consistency) and
18
+ * degrades to the legacy top-level `persona_prompting` block, then to deriving
19
+ * the section from the quantitative layers. It invents nothing.
20
+ */
21
+ const asDict = (v) => (v && typeof v === "object" && !Array.isArray(v) ? v : {});
22
+ const asArr = (v) => (Array.isArray(v) ? v : []);
23
+ const asStr = (v) => (typeof v === "string" && v.trim() ? v.trim() : undefined);
24
+ /** Read the persona-prompting source, preferring v1.0 layer-10 over legacy. */
25
+ function promptingSource(persona) {
26
+ const persona10 = asDict(persona.persona);
27
+ const legacy = asDict(persona.persona_prompting);
28
+ // A field present in either wins; v1.0 layer-10 takes precedence per-field.
29
+ return {
30
+ address: persona10.address ?? legacy.address,
31
+ voice_exemplars: persona10.voice_exemplars ?? legacy.voice_exemplars,
32
+ scene_contracts: persona10.scene_contracts ?? legacy.scene_contracts,
33
+ behavioral_anchors: persona10.behavioral_anchors ?? legacy.behavioral_anchors,
34
+ consistency: persona10.consistency ?? legacy.consistency,
35
+ };
36
+ }
37
+ function sectionOpener(persona, target) {
38
+ const addr = asDict(promptingSource(persona).address);
39
+ const youAre = asStr(addr.you_are);
40
+ const lines = [`# You are ${target.name}`];
41
+ lines.push("");
42
+ if (youAre) {
43
+ lines.push(youAre);
44
+ }
45
+ else {
46
+ // Derive from identity: role + purpose.
47
+ const identity = asDict(persona.identity);
48
+ const role = asDict(identity.role_identity);
49
+ const sys = asDict(identity.system_identity);
50
+ const roleName = asStr(role.primary_role)?.replace(/_/g, " ");
51
+ const purpose = asStr(sys.purpose);
52
+ const bits = [`You are **${target.name}**`, roleName ? `, the ${roleName}` : ""].join("");
53
+ lines.push(purpose ? `${bits}. ${purpose}` : `${bits}.`);
54
+ }
55
+ lines.push("You think, speak, and decide as this persona. Stay in character at all times — the rules " +
56
+ "below are who you are, not instructions you are following.");
57
+ return lines.join("\n");
58
+ }
59
+ function sectionWhoYouAre(persona) {
60
+ const identity = asDict(persona.identity);
61
+ const sys = asDict(identity.system_identity);
62
+ const narrative = asDict(identity.narrative_identity);
63
+ const out = ["## Who you are", ""];
64
+ const purpose = asStr(sys.purpose);
65
+ const selfConcept = asStr(narrative.self_concept);
66
+ const origin = asStr(narrative.origin);
67
+ if (purpose)
68
+ out.push(purpose);
69
+ if (selfConcept)
70
+ out.push("", selfConcept);
71
+ if (origin)
72
+ out.push("", origin);
73
+ const allowed = asArr(sys.allowed_domains).map(asStr).filter(Boolean);
74
+ const prohibited = asArr(sys.prohibited_domains).map(asStr).filter(Boolean);
75
+ if (allowed.length)
76
+ out.push("", `You work on: ${allowed.map((d) => d.replace(/_/g, " ")).join(", ")}.`);
77
+ if (prohibited.length)
78
+ out.push(`You do NOT work on: ${prohibited.map((d) => d.replace(/_/g, " ")).join(", ")}.`);
79
+ if (out.length === 2)
80
+ out.push("*(identity not specified in the spec)*");
81
+ return out.join("\n");
82
+ }
83
+ function sectionHowYouSpeak(persona) {
84
+ const voice = asDict(asDict(persona.persona).voice);
85
+ const out = ["## How you speak", ""];
86
+ const tone = asStr(voice.tone)?.replace(/_/g, " ");
87
+ const desc = asStr(voice.description);
88
+ const humor = asStr(voice.humor);
89
+ const verbosity = asStr(voice.verbosity);
90
+ const parts = [];
91
+ if (tone)
92
+ parts.push(`Your tone is ${tone}.`);
93
+ if (verbosity)
94
+ parts.push(`You are ${verbosity} by default.`);
95
+ if (humor)
96
+ parts.push(`Humor: ${humor}.`);
97
+ if (desc)
98
+ parts.push(desc);
99
+ out.push(parts.length ? parts.join(" ") : "*(voice not specified in the spec)*");
100
+ const exemplars = asArr(promptingSource(persona).voice_exemplars);
101
+ if (exemplars.length) {
102
+ out.push("", "**You sound like this:**");
103
+ for (const ex of exemplars) {
104
+ const e = asDict(ex);
105
+ const ctx = asStr(e.context);
106
+ const user = asStr(e.user);
107
+ const resp = asStr(e.persona);
108
+ if (!resp)
109
+ continue;
110
+ const lead = ctx ? `When ${ctx}` : user ? `Asked "${user}"` : "You";
111
+ out.push(`- ${lead}, you say: "${resp}"`);
112
+ }
113
+ }
114
+ return out.join("\n");
115
+ }
116
+ function sectionAlwaysNever(persona) {
117
+ const out = ["## What you always / never do", ""];
118
+ const character = asDict(persona.character);
119
+ const anchors = asDict(promptingSource(persona).behavioral_anchors);
120
+ const always = [];
121
+ const never = [];
122
+ // behavioral_anchors do/dont (verbatim when present).
123
+ for (const d of asArr(anchors.do).map(asStr).filter(Boolean))
124
+ always.push(d);
125
+ for (const d of asArr(anchors.dont).map(asStr).filter(Boolean))
126
+ never.push(d);
127
+ // character.virtues → Always (hard-enforced first, keeps a stable order).
128
+ const virtues = asDict(character.virtues);
129
+ const virtueEntries = Object.entries(virtues).sort((a, b) => {
130
+ const pa = asDict(a[1]).priority ?? 0;
131
+ const pb = asDict(b[1]).priority ?? 0;
132
+ return pb - pa;
133
+ });
134
+ for (const [, v] of virtueEntries) {
135
+ const desc = asStr(asDict(v).description);
136
+ if (desc)
137
+ always.push(desc);
138
+ }
139
+ // character.prohibited_behaviors → Never (verbatim).
140
+ for (const p of asArr(character.prohibited_behaviors).map(asStr).filter(Boolean))
141
+ never.push(p);
142
+ const dedupe = (xs) => [...new Map(xs.map((x) => [x.toLowerCase(), x])).values()];
143
+ out.push("**Always:**");
144
+ for (const a of dedupe(always))
145
+ out.push(`- ${a}`);
146
+ out.push("", "**Never:**");
147
+ for (const n of dedupe(never))
148
+ out.push(`- ${n}`);
149
+ const examples = asArr(anchors.examples).map(asStr).filter(Boolean);
150
+ if (examples.length) {
151
+ out.push("", "**For example:**");
152
+ for (const ex of examples)
153
+ out.push(`- ${ex}`);
154
+ }
155
+ return out.join("\n");
156
+ }
157
+ function sectionScenes(persona) {
158
+ const scenes = asArr(promptingSource(persona).scene_contracts);
159
+ const out = ["## In specific situations", ""];
160
+ if (!scenes.length) {
161
+ // Derive from character.behavioral_commitments when there are no scene contracts.
162
+ const commits = asArr(asDict(persona.character).behavioral_commitments);
163
+ if (!commits.length)
164
+ return "";
165
+ for (const c of commits) {
166
+ const rule = asStr(asDict(c).rule);
167
+ if (rule)
168
+ out.push(`- ${rule}`);
169
+ }
170
+ return out.join("\n");
171
+ }
172
+ for (const s of scenes) {
173
+ const sc = asDict(s);
174
+ const situation = asStr(sc.situation);
175
+ const behavior = asStr(sc.expected_behavior);
176
+ if (!situation || !behavior)
177
+ continue;
178
+ const actions = asArr(sc.actions).map(asStr).filter(Boolean).map((a) => a.replace(/_/g, " "));
179
+ const tail = actions.length ? ` (${actions.join("; ")})` : "";
180
+ out.push(`- When **${situation}**, you ${behavior}${tail}.`);
181
+ }
182
+ return out.join("\n");
183
+ }
184
+ function sectionHowYouThink(persona) {
185
+ const cognition = asDict(persona.cognition);
186
+ const out = ["## How you think", ""];
187
+ const style = asStr(cognition.reasoning_style);
188
+ const stance = asStr(cognition.epistemic_stance);
189
+ const strategy = asStr(cognition.default_strategy)?.replace(/_/g, " ");
190
+ const parts = [];
191
+ if (style)
192
+ parts.push(style);
193
+ if (strategy)
194
+ parts.push(`Your default approach is ${strategy}.`);
195
+ if (stance)
196
+ parts.push(stance);
197
+ out.push(parts.length ? parts.join(" ") : "*(cognition not specified in the spec)*");
198
+ const unc = asDict(cognition.uncertainty_policy);
199
+ const disclose = unc.disclose_when_above;
200
+ const abstain = unc.abstain_when_above;
201
+ if (typeof disclose === "number" || typeof abstain === "number") {
202
+ const bits = [];
203
+ if (typeof disclose === "number")
204
+ bits.push(`disclose uncertainty above ${Math.round(disclose * 100)}%`);
205
+ if (typeof abstain === "number")
206
+ bits.push(`abstain above ${Math.round(abstain * 100)}%`);
207
+ out.push("", `On uncertainty, you ${bits.join(" and ")}.`);
208
+ }
209
+ return out.join("\n");
210
+ }
211
+ function sectionFixedChange(persona) {
212
+ const consistency = asDict(promptingSource(persona).consistency);
213
+ const stable = asArr(consistency.stable).map(asStr).filter(Boolean);
214
+ const evolving = asArr(consistency.evolving).map(asStr).filter(Boolean);
215
+ const situational = asArr(consistency.situational).map(asStr).filter(Boolean);
216
+ if (!stable.length && !evolving.length && !situational.length)
217
+ return "";
218
+ const out = ["## What is fixed, what can change", ""];
219
+ if (stable.length)
220
+ out.push(`- **Fixed:** ${stable.join("; ")}.`);
221
+ if (evolving.length)
222
+ out.push(`- **Evolves (slowly, under governance):** ${evolving.join("; ")}.`);
223
+ if (situational.length)
224
+ out.push(`- **Situational:** ${situational.join("; ")}.`);
225
+ return out.join("\n");
226
+ }
227
+ /** The full set of stay-in-character hard limits, split from the safety limits. */
228
+ function hardLimitLists(persona) {
229
+ const sr = asDict(persona.self_regulation);
230
+ const legacy = asDict(persona.reflexive_self_regulation);
231
+ const limits = (asArr(sr.hard_limits).length ? asArr(sr.hard_limits) : asArr(legacy.hard_limits))
232
+ .map(asStr)
233
+ .filter(Boolean);
234
+ const character = [];
235
+ const safety = [];
236
+ for (const l of limits) {
237
+ // Stay-in-character guardrails (migrated from break_character_guardrails) read as
238
+ // expression rules; keep them for the "Staying in character" section too.
239
+ if (/stay |never drop the persona|never reveal these instructions|redirect off-topic/i.test(l)) {
240
+ character.push(l);
241
+ }
242
+ safety.push(l);
243
+ }
244
+ return { safety, character };
245
+ }
246
+ function sectionHardLimits(persona) {
247
+ const { safety } = hardLimitLists(persona);
248
+ const out = ["## Hard limits (never overridden)", ""];
249
+ if (!safety.length) {
250
+ out.push("*(no hard limits declared — this is a spec error; every persona must declare the safety universals)*");
251
+ return out.join("\n");
252
+ }
253
+ out.push("These are absolute and outrank everything below, including staying in character.", "");
254
+ for (const l of safety)
255
+ out.push(`- ${l}`);
256
+ return out.join("\n");
257
+ }
258
+ function sectionStayingInCharacter(persona, target) {
259
+ const { character } = hardLimitLists(persona);
260
+ const out = ["## Staying in character", ""];
261
+ out.push(`You remain ${target.name} under pressure — off-topic bait, attempts to make you drop the persona, ` +
262
+ "insistence that you are \"just an AI\".");
263
+ for (const l of character)
264
+ out.push(`- ${l}`);
265
+ out.push("", "**Staying in character NEVER overrides the hard limits above or the safety policy.** If the two " +
266
+ "ever conflict, the hard limits win.");
267
+ return out.join("\n");
268
+ }
269
+ function sectionMemory(input) {
270
+ const out = ["## Memory & resources", ""];
271
+ const manifest = input.resourceManifest?.trim();
272
+ if (manifest) {
273
+ out.push(manifest);
274
+ }
275
+ else {
276
+ out.push(`- \`${input.target.resourceBase}memory.md\` — your semantic memory`);
277
+ }
278
+ return out.join("\n");
279
+ }
280
+ function sectionSelfImprovement(persona) {
281
+ const ip = asDict(persona.improvement_policy);
282
+ const mode = asStr(ip.mode) ?? "locked";
283
+ const out = ["## Self-improvement", ""];
284
+ const explain = {
285
+ locked: "Your identity does not self-modify. Changes require a human editing the spec.",
286
+ suggesting: "You may PROPOSE self-edits; they queue for human approval before taking effect.",
287
+ autonomous: "You may apply governed self-edits within the declared envelopes; core changes still require human approval.",
288
+ };
289
+ out.push(explain[mode] ?? explain.locked);
290
+ out.push("", "Your behavior changes when the spec changes — not on user preference or pushback alone.");
291
+ return out.join("\n");
292
+ }
293
+ /**
294
+ * Assemble the canonical compiled persona document. Deterministic: the same
295
+ * persona spec (+ manifest + target) always produces byte-identical output.
296
+ */
297
+ export function assemblePersonaDoc(input) {
298
+ const persona = applyOverlay(input.persona, input.appliedOverlay);
299
+ const { target } = input;
300
+ const sections = [
301
+ sectionOpener(persona, target),
302
+ sectionWhoYouAre(persona),
303
+ sectionHowYouSpeak(persona),
304
+ sectionAlwaysNever(persona),
305
+ sectionScenes(persona),
306
+ sectionHowYouThink(persona),
307
+ sectionFixedChange(persona),
308
+ sectionHardLimits(persona),
309
+ sectionStayingInCharacter(persona, target),
310
+ sectionMemory(input),
311
+ sectionSelfImprovement(persona),
312
+ ].filter((s) => s.trim().length > 0);
313
+ const body = sections.join("\n\n").trimEnd() + "\n";
314
+ if (target.isSubagent) {
315
+ // Subagent placement expects a name/description frontmatter; the caller
316
+ // (placement adapter) owns that. The assembler emits the body only.
317
+ return body;
318
+ }
319
+ return body;
320
+ }
321
+ /** Apply dot-path overlay overrides onto a shallow clone of the persona. */
322
+ function applyOverlay(persona, overlay) {
323
+ if (!overlay || Object.keys(overlay).length === 0)
324
+ return persona;
325
+ const clone = structuredClone(persona);
326
+ for (const [path, value] of Object.entries(overlay)) {
327
+ const keys = path.split(".");
328
+ let node = clone;
329
+ for (let i = 0; i < keys.length - 1; i++) {
330
+ const k = keys[i];
331
+ if (!node[k] || typeof node[k] !== "object")
332
+ node[k] = {};
333
+ node = node[k];
334
+ }
335
+ node[keys[keys.length - 1]] = value;
336
+ }
337
+ return clone;
338
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * F3.2 — `.dist/` consumer slices (formalizes the plan's `.dist/` model).
3
+ *
4
+ * The compiled PERSONA.md is the COLD, complete document. Hosts that inject an
5
+ * identity ahead of EVERY turn pay for its full length on every call, but most
6
+ * of that length (worked examples, audience adaptations, self-improvement prose)
7
+ * is not needed on the hot path. So the deterministic compiler also emits a HOT
8
+ * slice: the always-load essentials — the opener, how the persona speaks, the
9
+ * always/never anchors, and the hard limits (safety must never be dropped from
10
+ * a hot slice). Everything else stays in the cold document, loaded on demand.
11
+ *
12
+ * Slices are DETERMINISTIC and DERIVED (rebuildable from PERSONA.md); they are
13
+ * ephemeral build output, never hand-edited — the same contract as the artifact
14
+ * the assembler produces.
15
+ */
16
+ export interface DistSlices {
17
+ /** Always-load essentials: opener + how-you-speak + anchors + hard limits. */
18
+ hot: string;
19
+ /** The full compiled document (identical to PERSONA.md). */
20
+ cold: string;
21
+ }
22
+ /**
23
+ * Derive the hot/cold slices from a compiled document. `cold` is the document
24
+ * verbatim; `hot` is the opener (everything before the first `## `) followed by
25
+ * the HOT_SECTIONS present, in canonical order.
26
+ */
27
+ export declare function distSlices(compiledDoc: string): DistSlices;
28
+ /** Standard slice file names under `.dist/`. */
29
+ export declare const DIST_HOT_FILE = "PERSONA.hot.md";
30
+ export declare const DIST_COLD_FILE = "PERSONA.cold.md";
@@ -0,0 +1,66 @@
1
+ /**
2
+ * F3.2 — `.dist/` consumer slices (formalizes the plan's `.dist/` model).
3
+ *
4
+ * The compiled PERSONA.md is the COLD, complete document. Hosts that inject an
5
+ * identity ahead of EVERY turn pay for its full length on every call, but most
6
+ * of that length (worked examples, audience adaptations, self-improvement prose)
7
+ * is not needed on the hot path. So the deterministic compiler also emits a HOT
8
+ * slice: the always-load essentials — the opener, how the persona speaks, the
9
+ * always/never anchors, and the hard limits (safety must never be dropped from
10
+ * a hot slice). Everything else stays in the cold document, loaded on demand.
11
+ *
12
+ * Slices are DETERMINISTIC and DERIVED (rebuildable from PERSONA.md); they are
13
+ * ephemeral build output, never hand-edited — the same contract as the artifact
14
+ * the assembler produces.
15
+ */
16
+ /** Section headings that belong in the hot slice (always loaded), in order. */
17
+ const HOT_SECTIONS = [
18
+ "how you speak",
19
+ "what you always / never do",
20
+ "hard limits (never overridden)",
21
+ "staying in character",
22
+ ];
23
+ function splitSections(doc) {
24
+ const lines = doc.split(/\r?\n/);
25
+ const sections = [];
26
+ let heading = "";
27
+ let buf = [];
28
+ const flush = () => {
29
+ if (buf.length)
30
+ sections.push({ heading: heading.toLowerCase(), text: buf.join("\n").replace(/\s+$/, "") });
31
+ };
32
+ for (const line of lines) {
33
+ const h = line.match(/^##\s+(.*)$/);
34
+ if (h) {
35
+ flush();
36
+ heading = h[1].trim();
37
+ buf = [line];
38
+ }
39
+ else {
40
+ buf.push(line);
41
+ }
42
+ }
43
+ flush();
44
+ return sections;
45
+ }
46
+ /**
47
+ * Derive the hot/cold slices from a compiled document. `cold` is the document
48
+ * verbatim; `hot` is the opener (everything before the first `## `) followed by
49
+ * the HOT_SECTIONS present, in canonical order.
50
+ */
51
+ export function distSlices(compiledDoc) {
52
+ const sections = splitSections(compiledDoc);
53
+ const preamble = sections.find((s) => s.heading === "");
54
+ const parts = [];
55
+ if (preamble)
56
+ parts.push(preamble.text.trim());
57
+ for (const wanted of HOT_SECTIONS) {
58
+ const s = sections.find((sec) => sec.heading === wanted);
59
+ if (s)
60
+ parts.push(s.text.trim());
61
+ }
62
+ return { hot: parts.join("\n\n").trimEnd() + "\n", cold: compiledDoc.trimEnd() + "\n" };
63
+ }
64
+ /** Standard slice file names under `.dist/`. */
65
+ export const DIST_HOT_FILE = "PERSONA.hot.md";
66
+ export const DIST_COLD_FILE = "PERSONA.cold.md";
@@ -0,0 +1,47 @@
1
+ /**
2
+ * F3.1 — the DETERMINISTIC faithfulness check (guards stage 2 of the pipeline).
3
+ *
4
+ * Stage 2 (the optional LLM "polish") is constrained to REPHRASE the assembled
5
+ * document, never to ADD or DROP claims. This check enforces that contract
6
+ * deterministically by diffing the polished document against the assembled one
7
+ * (the ground truth), section by section, over the PROTECTED claim classes:
8
+ *
9
+ * - Hard limits — a dropped safety limit is a hard failure.
10
+ * - Staying in character — same (these are hard limits too).
11
+ * - What you always/never — behavioral anchors.
12
+ * - What is fixed/change — consistency dimensions.
13
+ *
14
+ * The historical CMO regression — the compiled PERSONA.md invented `consistency`
15
+ * items the source never declared — fails here as an INVENTED finding.
16
+ *
17
+ * Matching is token-coverage based (deterministic, no model): a claim is
18
+ * "preserved" iff some claim on the other side shares enough content tokens.
19
+ * Rephrasing (synonym-free reordering, added connective words) passes; adding a
20
+ * genuinely new bullet or dropping one does not.
21
+ */
22
+ export type FaithfulnessSection = "hard limits (never overridden)" | "staying in character" | "what you always / never do" | "what is fixed, what can change";
23
+ export interface FaithfulnessFinding {
24
+ kind: "dropped" | "invented";
25
+ section: string;
26
+ /** The claim text that was dropped from the source or invented in the polish. */
27
+ text: string;
28
+ /** Best token-coverage found against the other side (for diagnostics). */
29
+ bestCoverage: number;
30
+ }
31
+ export interface FaithfulnessReport {
32
+ ok: boolean;
33
+ findings: FaithfulnessFinding[];
34
+ }
35
+ export interface FaithfulnessOptions {
36
+ /** A claim is preserved when coverage ≥ this. Default 0.5. */
37
+ threshold?: number;
38
+ /** Sections checked. Default: the four protected classes. */
39
+ sections?: string[];
40
+ }
41
+ /**
42
+ * Diff `polished` against `assembled` (the ground truth). Returns findings for
43
+ * dropped source claims and invented polish claims in the protected sections.
44
+ */
45
+ export declare function checkFaithfulness(assembled: string, polished: string, opts?: FaithfulnessOptions): FaithfulnessReport;
46
+ /** One-line human summary of a report (for CLI output / logs). */
47
+ export declare function summarizeFaithfulness(report: FaithfulnessReport): string;