@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,88 @@
1
+ /**
2
+ * Persona file IO for the engine.
3
+ *
4
+ * The engine operates on a known PERSONA.md path (the active persona's compiled
5
+ * document) and its sibling state.json. Frontmatter carries the quantitative
6
+ * envelopes; state.json carries the mutable current values + mutation_log.
7
+ *
8
+ * This is intentionally narrow: path *resolution* (root vs subagent slugs) lives
9
+ * in the CLI's load.ts. The engine just reads/writes at given paths.
10
+ */
11
+ /**
12
+ * Current state.json schema version (schema/state.schema.json's newest accepted
13
+ * value — 0.9.0 added the optional agent_session block; 0.10 changed no state
14
+ * fields). Single source for every seeder; keep in sync with the schema enum.
15
+ */
16
+ export declare const STATE_SCHEMA_VERSION = "1.0.0";
17
+ export interface PersonaFrontmatter {
18
+ [key: string]: unknown;
19
+ }
20
+ export interface MutationLogEntry {
21
+ ts: string;
22
+ field: string;
23
+ from: number;
24
+ to: number;
25
+ delta_requested: number;
26
+ clamped: boolean;
27
+ reason: string;
28
+ actor: "actor-llm" | "runtime-decay" | "runtime-context" | "human-operator" | "judge-correction";
29
+ tool_call_id?: string;
30
+ governance_blocked?: boolean;
31
+ /** v0.8: machine/instance that produced this mutation (cross-OS reconciliation). */
32
+ origin_node?: string;
33
+ /** v0.8: runtime session id, for traceability. */
34
+ session_id?: string;
35
+ }
36
+ export interface StateFile {
37
+ schema_version: string;
38
+ persona_id: string;
39
+ persona_version: string;
40
+ session_id?: string;
41
+ values: Record<string, number>;
42
+ active_context?: {
43
+ task_mode: string | null;
44
+ audience: string | null;
45
+ additional_context_flags?: string[];
46
+ };
47
+ memory_anchors_active?: string[];
48
+ mutation_log: MutationLogEntry[];
49
+ last_compiled_at?: string | null;
50
+ last_compiled_hash?: string | null;
51
+ /** v0.9: live agent-loop session tracking (resumable across runs). */
52
+ agent_session?: {
53
+ active_task: string | null;
54
+ started_at: string | null;
55
+ step_count: number;
56
+ token_count: number;
57
+ cost_usd: number;
58
+ stop_reason: string | null;
59
+ };
60
+ }
61
+ /** A persona handle: resolved paths + parsed frontmatter. */
62
+ export interface PersonaHandle {
63
+ /** Path to the compiled PERSONA.md (or .claude/agents/<slug>.md). */
64
+ personaPath: string;
65
+ /** Path to sibling state.json. */
66
+ statePath: string;
67
+ /** Parsed YAML frontmatter from PERSONA.md (quantitative layers + envelopes). */
68
+ frontmatter: PersonaFrontmatter;
69
+ /** Raw markdown body (the qualitative compiled document). */
70
+ body: string;
71
+ }
72
+ export declare function loadPersona(personaPathArg: string): PersonaHandle;
73
+ export declare function readState(statePath: string): StateFile;
74
+ /**
75
+ * Atomic write: temp file + rename in the same directory, so concurrent readers
76
+ * (dash polling, another CLI) always see a complete JSON document — never a torn
77
+ * partial write. Serialization of read→modify→write sequences is the caller's job
78
+ * via withStateLock (see lock.ts).
79
+ */
80
+ export declare function writeState(statePath: string, state: StateFile): void;
81
+ export declare function stateExists(statePath: string): boolean;
82
+ /** The human-facing name of a persona, from its frontmatter. */
83
+ export declare function displayName(fm: PersonaFrontmatter): string;
84
+ /**
85
+ * Return the persona's state, seeding a fresh state.json from envelope means if
86
+ * none exists yet. Keeps hosts (REPL, MCP) working out-of-the-box.
87
+ */
88
+ export declare function ensureState(handle: PersonaHandle): StateFile;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Persona file IO for the engine.
3
+ *
4
+ * The engine operates on a known PERSONA.md path (the active persona's compiled
5
+ * document) and its sibling state.json. Frontmatter carries the quantitative
6
+ * envelopes; state.json carries the mutable current values + mutation_log.
7
+ *
8
+ * This is intentionally narrow: path *resolution* (root vs subagent slugs) lives
9
+ * in the CLI's load.ts. The engine just reads/writes at given paths.
10
+ */
11
+ import { readFileSync, writeFileSync, existsSync, renameSync } from "node:fs";
12
+ import { dirname, join, resolve } from "node:path";
13
+ import matter from "gray-matter";
14
+ import { extractEnvelopes } from "./envelopes.js";
15
+ import { withStateLock } from "./lock.js";
16
+ /**
17
+ * Current state.json schema version (schema/state.schema.json's newest accepted
18
+ * value — 0.9.0 added the optional agent_session block; 0.10 changed no state
19
+ * fields). Single source for every seeder; keep in sync with the schema enum.
20
+ */
21
+ export const STATE_SCHEMA_VERSION = "1.0.0";
22
+ export function loadPersona(personaPathArg) {
23
+ const personaPath = resolve(personaPathArg);
24
+ if (!existsSync(personaPath)) {
25
+ throw new Error(`PERSONA.md not found at ${personaPath}`);
26
+ }
27
+ const raw = readFileSync(personaPath, "utf-8");
28
+ const parsed = matter(raw);
29
+ return {
30
+ personaPath,
31
+ statePath: join(dirname(personaPath), "state.json"),
32
+ frontmatter: (parsed.data ?? {}),
33
+ body: parsed.content ?? "",
34
+ };
35
+ }
36
+ export function readState(statePath) {
37
+ if (!existsSync(statePath)) {
38
+ throw new Error(`state.json not found at ${statePath}. Run 'personaxis state init' first.`);
39
+ }
40
+ return JSON.parse(readFileSync(statePath, "utf-8"));
41
+ }
42
+ /**
43
+ * Atomic write: temp file + rename in the same directory, so concurrent readers
44
+ * (dash polling, another CLI) always see a complete JSON document — never a torn
45
+ * partial write. Serialization of read→modify→write sequences is the caller's job
46
+ * via withStateLock (see lock.ts).
47
+ */
48
+ export function writeState(statePath, state) {
49
+ const tmp = `${statePath}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
50
+ writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n", "utf-8");
51
+ renameSync(tmp, statePath);
52
+ }
53
+ export function stateExists(statePath) {
54
+ return existsSync(statePath);
55
+ }
56
+ /** The human-facing name of a persona, from its frontmatter. */
57
+ export function displayName(fm) {
58
+ const id = fm.identity;
59
+ const meta = fm.metadata;
60
+ return id?.display_name ?? meta?.name ?? id?.canonical_id ?? "persona";
61
+ }
62
+ /**
63
+ * Return the persona's state, seeding a fresh state.json from envelope means if
64
+ * none exists yet. Keeps hosts (REPL, MCP) working out-of-the-box.
65
+ */
66
+ export function ensureState(handle) {
67
+ if (existsSync(handle.statePath))
68
+ return readState(handle.statePath);
69
+ // Seeding races with other processes seeding the same persona — take the lock and
70
+ // re-check so exactly one seeder wins.
71
+ return withStateLock(handle.statePath, () => {
72
+ if (existsSync(handle.statePath))
73
+ return readState(handle.statePath);
74
+ const env = extractEnvelopes(handle.frontmatter);
75
+ const meta = (handle.frontmatter.metadata ?? {});
76
+ const values = {};
77
+ for (const [k, e] of Object.entries(env.envelopes))
78
+ values[k] = e.mean;
79
+ const state = {
80
+ schema_version: STATE_SCHEMA_VERSION,
81
+ persona_id: meta.name ?? "persona",
82
+ persona_version: meta.version ?? "0.0.0",
83
+ values,
84
+ active_context: { task_mode: null, audience: null, additional_context_flags: [] },
85
+ memory_anchors_active: [],
86
+ mutation_log: [],
87
+ last_compiled_at: null,
88
+ last_compiled_hash: null,
89
+ };
90
+ writeState(handle.statePath, state);
91
+ return state;
92
+ });
93
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * F3.3 — storage ports (the hexagonal seam, "only where it hurts": persistence).
3
+ *
4
+ * The engine's spec-faithful logic — clamp+audit mutation, the governance gate,
5
+ * the hash-chained ledger, the Living Loop — is pure and unchanged. What varies
6
+ * between a local CLI and the managed SaaS is only WHERE bytes live: the local
7
+ * default is a git-versionable persona folder (atomic writes + a per-persona
8
+ * lock); the SaaS is Postgres/S3 over the SAME engine. These ports are that
9
+ * boundary, and `defaultFsStorage()` is the reference (filesystem) adapter.
10
+ *
11
+ * Ports (each keyed by the persona's path/id so one engine can host many):
12
+ * - LockProvider — serialize read→modify→write (same-machine default; a
13
+ * distributed lock in the SaaS);
14
+ * - StateStore — state.json (the runtime checkpoint);
15
+ * - MemoryStore — semantic memory (memory.md) + consolidation;
16
+ * - LedgerStore — the append-only, hash-chained EPISODIC ledger
17
+ * (tamper-evident; append + read + verify + redact);
18
+ * - ModelClient — the LLM call the compiler/appraiser makes (the SaaS
19
+ * injects its hosted model; the loop's appraiser is the
20
+ * existing model seam and stays injectable separately).
21
+ */
22
+ import type { StateFile } from "../persona.js";
23
+ import type { MemoryEntry } from "../memory.js";
24
+ export interface LockProvider {
25
+ /** Run `fn` while holding the exclusive lock for `key` (a persona's state path). */
26
+ withLock<T>(key: string, fn: () => T): T;
27
+ }
28
+ export interface StateStore {
29
+ read(key: string): StateFile;
30
+ write(key: string, state: StateFile): void;
31
+ exists(key: string): boolean;
32
+ }
33
+ export interface MemoryStore {
34
+ /** The curated long-term semantic memory (memory.md) as text. */
35
+ readSemantic(key: string): string;
36
+ /** Consolidate recent episodic entries into semantic memory. */
37
+ consolidate(key: string, limit?: number): {
38
+ ok: boolean;
39
+ path: string;
40
+ count: number;
41
+ };
42
+ }
43
+ export interface ChainVerification {
44
+ ok: boolean;
45
+ brokenAt?: number;
46
+ }
47
+ export interface LedgerStore {
48
+ /** All episodic entries (the hash-chained ledger), oldest→newest. */
49
+ read(key: string): MemoryEntry[];
50
+ /** Append one entry, extending the hash chain. */
51
+ append(key: string, entry: MemoryEntry): void;
52
+ /** Verify the chain is intact (tamper-evidence). */
53
+ verify(key: string): ChainVerification;
54
+ /** Erase an entry's content while keeping the chain verifiable (right-to-delete). */
55
+ redact(key: string, id: string, reason: string): {
56
+ redacted: boolean;
57
+ };
58
+ }
59
+ export interface ModelCompletion {
60
+ text: string;
61
+ model: string;
62
+ }
63
+ export interface ModelClient {
64
+ complete(prompt: string, opts?: {
65
+ timeoutMs?: number;
66
+ }): Promise<ModelCompletion>;
67
+ }
68
+ /** The full storage bundle an engine host can inject. `model` is optional. */
69
+ export interface Storage {
70
+ lock: LockProvider;
71
+ state: StateStore;
72
+ memory: MemoryStore;
73
+ ledger: LedgerStore;
74
+ model?: ModelClient;
75
+ }
76
+ export declare const fsLockProvider: LockProvider;
77
+ export declare const fsStateStore: StateStore;
78
+ export declare const fsMemoryStore: MemoryStore;
79
+ export declare const fsLedgerStore: LedgerStore;
80
+ /** The default filesystem storage bundle (git-versionable persona folder). */
81
+ export declare function defaultFsStorage(): Storage;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * F3.3 — storage ports (the hexagonal seam, "only where it hurts": persistence).
3
+ *
4
+ * The engine's spec-faithful logic — clamp+audit mutation, the governance gate,
5
+ * the hash-chained ledger, the Living Loop — is pure and unchanged. What varies
6
+ * between a local CLI and the managed SaaS is only WHERE bytes live: the local
7
+ * default is a git-versionable persona folder (atomic writes + a per-persona
8
+ * lock); the SaaS is Postgres/S3 over the SAME engine. These ports are that
9
+ * boundary, and `defaultFsStorage()` is the reference (filesystem) adapter.
10
+ *
11
+ * Ports (each keyed by the persona's path/id so one engine can host many):
12
+ * - LockProvider — serialize read→modify→write (same-machine default; a
13
+ * distributed lock in the SaaS);
14
+ * - StateStore — state.json (the runtime checkpoint);
15
+ * - MemoryStore — semantic memory (memory.md) + consolidation;
16
+ * - LedgerStore — the append-only, hash-chained EPISODIC ledger
17
+ * (tamper-evident; append + read + verify + redact);
18
+ * - ModelClient — the LLM call the compiler/appraiser makes (the SaaS
19
+ * injects its hosted model; the loop's appraiser is the
20
+ * existing model seam and stays injectable separately).
21
+ */
22
+ import { readState, writeState, stateExists, } from "../persona.js";
23
+ import { withStateLock } from "../lock.js";
24
+ import { readMemory, commitMemoryEntry, verifyMemoryChain, redactMemory, readSemanticMemory, consolidateSemantic, } from "../memory.js";
25
+ // ── default filesystem adapters (the reference implementation) ──────────────────
26
+ export const fsLockProvider = {
27
+ withLock: (key, fn) => withStateLock(key, fn),
28
+ };
29
+ export const fsStateStore = {
30
+ read: (key) => readState(key),
31
+ write: (key, state) => writeState(key, state),
32
+ exists: (key) => stateExists(key),
33
+ };
34
+ export const fsMemoryStore = {
35
+ readSemantic: (key) => readSemanticMemory(key),
36
+ consolidate: (key, limit) => consolidateSemantic(key, limit),
37
+ };
38
+ export const fsLedgerStore = {
39
+ read: (key) => readMemory(key),
40
+ append: (key, entry) => commitMemoryEntry(key, entry),
41
+ verify: (key) => verifyMemoryChain(key),
42
+ redact: (key, id, reason) => {
43
+ redactMemory(key, id, reason); // returns the redacted+audit entries or throws
44
+ return { redacted: true };
45
+ },
46
+ };
47
+ /** The default filesystem storage bundle (git-versionable persona folder). */
48
+ export function defaultFsStorage() {
49
+ return { lock: fsLockProvider, state: fsStateStore, memory: fsMemoryStore, ledger: fsLedgerStore };
50
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Provenance & sensitive-action gates (F3 — plan/11-security).
3
+ *
4
+ * The memory-poisoning literature shows that "internal state = trusted" is a
5
+ * false assumption: untrusted content laundered through memory drives later
6
+ * "trusted" actions (Yang et al., 2026; Dash et al., 2026). Defenses here:
7
+ * - rank every source by trust;
8
+ * - gate sensitive actions on the *provenance of their justification* — if the
9
+ * reasoning traces to an untrusted channel, the action is refused;
10
+ * - a lightweight consensus check flags contradictory memory (A-MemGuard-style).
11
+ */
12
+ import type { ProvenanceSource } from "./appraisal.js";
13
+ import type { MemoryEntry } from "./memory.js";
14
+ /** Higher = more trusted. tool/synthesis are the poisoning-prone channels. */
15
+ export declare const TRUST: Record<ProvenanceSource, number>;
16
+ export type SensitiveAction = "delete" | "external_api" | "credential_use" | "self_edit" | "file_write";
17
+ /** Minimum justification trust required to dispatch each sensitive action. */
18
+ export declare const ACTION_MIN_TRUST: Record<SensitiveAction, number>;
19
+ export interface GateResult {
20
+ allowed: boolean;
21
+ action: SensitiveAction;
22
+ minTrust: number;
23
+ justificationTrust: number;
24
+ reason: string;
25
+ }
26
+ /**
27
+ * Decide whether a sensitive action may proceed given the provenance of the
28
+ * memory/observations justifying it. The weakest link wins: a single untrusted
29
+ * source in the justification chain caps the trust.
30
+ */
31
+ export declare function sensitiveActionGate(action: SensitiveAction, justificationSources: ProvenanceSource[]): GateResult;
32
+ export interface Anomaly {
33
+ kind: "contradiction" | "untrusted-spike" | "model-flagged";
34
+ detail: string;
35
+ entries: string[];
36
+ }
37
+ export interface AnomalyConfig {
38
+ /** Optional model classifier (A-MemGuard-style); fused with the heuristics. */
39
+ classifier?: (entry: MemoryEntry) => {
40
+ score: number;
41
+ label?: string;
42
+ };
43
+ /** Score at/above which a classifier hit becomes an anomaly. Default 0.7. */
44
+ threshold?: number;
45
+ }
46
+ /**
47
+ * Consensus / anomaly pass over recent memory. Explainable heuristics (negating
48
+ * statements, bursts of untrusted writes) PLUS an optional model-based check
49
+ * (Wei et al., 2025, A-MemGuard) fused in via `config.classifier`.
50
+ */
51
+ export declare function detectMemoryAnomalies(entries: MemoryEntry[], config?: AnomalyConfig): Anomaly[];
52
+ export type ConsensusVerdict = "consistent" | "conflicting" | "insufficient";
53
+ export interface MemoryConsensus {
54
+ verdict: ConsensusVerdict;
55
+ supporting: string[];
56
+ contradicting: string[];
57
+ }
58
+ /**
59
+ * Multi-path consistency check (A-MemGuard): before a risky decision, ask whether
60
+ * the memory set agrees about a claim. Returns supporting vs contradicting entries
61
+ * and a verdict. `conflicting` is the signal to abstain / seek confirmation.
62
+ */
63
+ export declare function memoryConsensus(entries: MemoryEntry[], claim: string): MemoryConsensus;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Provenance & sensitive-action gates (F3 — plan/11-security).
3
+ *
4
+ * The memory-poisoning literature shows that "internal state = trusted" is a
5
+ * false assumption: untrusted content laundered through memory drives later
6
+ * "trusted" actions (Yang et al., 2026; Dash et al., 2026). Defenses here:
7
+ * - rank every source by trust;
8
+ * - gate sensitive actions on the *provenance of their justification* — if the
9
+ * reasoning traces to an untrusted channel, the action is refused;
10
+ * - a lightweight consensus check flags contradictory memory (A-MemGuard-style).
11
+ */
12
+ /** Higher = more trusted. tool/synthesis are the poisoning-prone channels. */
13
+ export const TRUST = {
14
+ user: 3,
15
+ internal: 2,
16
+ synthesis: 1,
17
+ tool: 1,
18
+ };
19
+ /** Minimum justification trust required to dispatch each sensitive action. */
20
+ export const ACTION_MIN_TRUST = {
21
+ delete: 3,
22
+ credential_use: 3,
23
+ external_api: 2,
24
+ self_edit: 3,
25
+ file_write: 2,
26
+ };
27
+ /**
28
+ * Decide whether a sensitive action may proceed given the provenance of the
29
+ * memory/observations justifying it. The weakest link wins: a single untrusted
30
+ * source in the justification chain caps the trust.
31
+ */
32
+ export function sensitiveActionGate(action, justificationSources) {
33
+ const minTrust = ACTION_MIN_TRUST[action];
34
+ const justificationTrust = justificationSources.length === 0
35
+ ? 0
36
+ : Math.min(...justificationSources.map((s) => TRUST[s]));
37
+ const allowed = justificationTrust >= minTrust;
38
+ return {
39
+ allowed,
40
+ action,
41
+ minTrust,
42
+ justificationTrust,
43
+ reason: allowed
44
+ ? `justification trust ${justificationTrust} >= required ${minTrust}`
45
+ : `justification trust ${justificationTrust} < required ${minTrust} (untrusted provenance)`,
46
+ };
47
+ }
48
+ /**
49
+ * Consensus / anomaly pass over recent memory. Explainable heuristics (negating
50
+ * statements, bursts of untrusted writes) PLUS an optional model-based check
51
+ * (Wei et al., 2025, A-MemGuard) fused in via `config.classifier`.
52
+ */
53
+ export function detectMemoryAnomalies(entries, config = {}) {
54
+ const anomalies = [];
55
+ const threshold = config.threshold ?? 0.7;
56
+ // 1. naive contradiction: "X" vs "not X" / "no X" on the same key phrase.
57
+ for (let i = 0; i < entries.length; i++) {
58
+ for (let j = i + 1; j < entries.length; j++) {
59
+ const a = entries[i].content.toLowerCase();
60
+ const b = entries[j].content.toLowerCase();
61
+ if (negates(a, b) || negates(b, a)) {
62
+ anomalies.push({
63
+ kind: "contradiction",
64
+ detail: `"${entries[i].content.slice(0, 40)}" vs "${entries[j].content.slice(0, 40)}"`,
65
+ entries: [entries[i].hash, entries[j].hash],
66
+ });
67
+ }
68
+ }
69
+ }
70
+ // 2. untrusted spike: >=3 consecutive untrusted (tool/synthesis) writes.
71
+ let run = [];
72
+ for (const e of entries) {
73
+ if (TRUST[e.source] <= 1)
74
+ run.push(e);
75
+ else
76
+ run = [];
77
+ if (run.length >= 3) {
78
+ anomalies.push({
79
+ kind: "untrusted-spike",
80
+ detail: `${run.length} consecutive low-trust memory writes`,
81
+ entries: run.map((r) => r.hash),
82
+ });
83
+ run = [];
84
+ }
85
+ }
86
+ // 3. model-based pass (A-MemGuard): fuse a classifier's per-entry judgment.
87
+ if (config.classifier) {
88
+ for (const e of entries) {
89
+ const c = config.classifier(e);
90
+ if (c.score >= threshold) {
91
+ anomalies.push({
92
+ kind: "model-flagged",
93
+ detail: `${c.label ?? "anomalous"} (score ${c.score.toFixed(2)})`,
94
+ entries: [e.hash],
95
+ });
96
+ }
97
+ }
98
+ }
99
+ return anomalies;
100
+ }
101
+ /**
102
+ * Multi-path consistency check (A-MemGuard): before a risky decision, ask whether
103
+ * the memory set agrees about a claim. Returns supporting vs contradicting entries
104
+ * and a verdict. `conflicting` is the signal to abstain / seek confirmation.
105
+ */
106
+ export function memoryConsensus(entries, claim) {
107
+ const claimLc = claim.toLowerCase();
108
+ const core = claimLc.replace(/^(the |a |an )/, "").trim();
109
+ const supporting = [];
110
+ const contradicting = [];
111
+ for (const e of entries) {
112
+ const c = e.content.toLowerCase();
113
+ const negated = c.includes("not " + core) || c.includes("no " + core) || c.includes("isn't " + core);
114
+ if (negated)
115
+ contradicting.push(e.hash);
116
+ else if (core.length >= 4 && c.includes(core))
117
+ supporting.push(e.hash);
118
+ }
119
+ const verdict = supporting.length > 0 && contradicting.length > 0
120
+ ? "conflicting"
121
+ : supporting.length + contradicting.length === 0
122
+ ? "insufficient"
123
+ : "consistent";
124
+ return { verdict, supporting, contradicting };
125
+ }
126
+ function negates(a, b) {
127
+ // crude: b is "not <a>" / "no <a>" form of a
128
+ const core = a.replace(/^(the |a |an )/, "").trim();
129
+ if (core.length < 6)
130
+ return false;
131
+ return b.includes("not " + core) || b.includes("no " + core) || b.includes("isn't " + core);
132
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Recompile-pending marker — the "stale" signal that closes the self-evolution loop.
3
+ *
4
+ * When a governed self-edit is APPLIED (overlay changes), the compiled PERSONA.md no longer
5
+ * reflects the spec. Core cannot compile (that needs an LLM, which lives in the CLI/host), so
6
+ * it drops a small sentinel next to personaxis.md. A consumer that CAN compile — the REPL
7
+ * (with a provider) or the host driving MCP — recompiles and clears it. `personaxis compile`
8
+ * clears it automatically; `compile --if-pending` is a cheap no-op when nothing is stale.
9
+ */
10
+ export interface RecompileState {
11
+ pending: boolean;
12
+ reason?: string;
13
+ ts?: string;
14
+ }
15
+ /** Mark the persona's compiled doc as stale (idempotent). */
16
+ export declare function markRecompilePending(personaPath: string, reason: string): void;
17
+ export declare function readRecompilePending(personaPath: string): RecompileState;
18
+ /** Clear the marker (called after a successful compile). */
19
+ export declare function clearRecompilePending(personaPath: string): void;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Recompile-pending marker — the "stale" signal that closes the self-evolution loop.
3
+ *
4
+ * When a governed self-edit is APPLIED (overlay changes), the compiled PERSONA.md no longer
5
+ * reflects the spec. Core cannot compile (that needs an LLM, which lives in the CLI/host), so
6
+ * it drops a small sentinel next to personaxis.md. A consumer that CAN compile — the REPL
7
+ * (with a provider) or the host driving MCP — recompiles and clears it. `personaxis compile`
8
+ * clears it automatically; `compile --if-pending` is a cheap no-op when nothing is stale.
9
+ */
10
+ import { writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
11
+ import { join, dirname } from "node:path";
12
+ function markerPath(personaPath) {
13
+ return join(dirname(personaPath), ".recompile-pending.json");
14
+ }
15
+ /** Mark the persona's compiled doc as stale (idempotent). */
16
+ export function markRecompilePending(personaPath, reason) {
17
+ writeFileSync(markerPath(personaPath), JSON.stringify({ pending: true, reason, ts: new Date().toISOString() }, null, 2) + "\n", "utf-8");
18
+ }
19
+ export function readRecompilePending(personaPath) {
20
+ const p = markerPath(personaPath);
21
+ if (!existsSync(p))
22
+ return { pending: false };
23
+ try {
24
+ const j = JSON.parse(readFileSync(p, "utf-8"));
25
+ return { pending: true, reason: j.reason, ts: j.ts };
26
+ }
27
+ catch {
28
+ return { pending: true };
29
+ }
30
+ }
31
+ /** Clear the marker (called after a successful compile). */
32
+ export function clearRecompilePending(personaPath) {
33
+ const p = markerPath(personaPath);
34
+ if (existsSync(p))
35
+ rmSync(p);
36
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Overseer registry + global/overlay persona model (F7 — plan/08-persona-model).
3
+ *
4
+ * The "master" personaxis-system is a governed *runtime* that is aware of every
5
+ * persona and project in the environment. It is NOT a feeling persona — it
6
+ * orchestrates and audits. This module is its memory:
7
+ *
8
+ * ~/.personaxis/ (override with PERSONAXIS_HOME)
9
+ * registry.json personas, projects, collections, machines
10
+ * personas/<slug>/ global identity + memory (reused across projects)
11
+ *
12
+ * Reuse model: a persona lives globally; each project "mounts" it with a local
13
+ * overlay (its own state.json + project memory). So the same persona can be
14
+ * reused WITH accumulated memory, or instantiated fresh per project. Collections
15
+ * (teams) group personas + projects. State is tracked per machine so the same
16
+ * user-clone can live on Windows/Linux/macOS and reconcile via git without one
17
+ * machine clobbering another.
18
+ */
19
+ export interface PersonaRecord {
20
+ slug: string;
21
+ globalPath: string;
22
+ createdTs: string;
23
+ }
24
+ export interface ProjectRecord {
25
+ root: string;
26
+ slugs: string[];
27
+ lastSeen: string;
28
+ machine: string;
29
+ }
30
+ /**
31
+ * A Collection is pure ORGANIZATION — a named group of personas/projects, like a
32
+ * folder or tag. No runtime behavior. (Distinct from a Team, below.)
33
+ */
34
+ export interface Collection {
35
+ name: string;
36
+ personas: string[];
37
+ projects: string[];
38
+ }
39
+ /**
40
+ * A Team is an OPERATIONAL multi-agent unit: personas with ROLES, a shared GOAL,
41
+ * that collaborate (e.g. via the blackboard, scoped to the team's members). A team
42
+ * has a lead and members; it is runtime, not just taxonomy.
43
+ */
44
+ export interface TeamMember {
45
+ slug: string;
46
+ role: string;
47
+ }
48
+ export interface Team {
49
+ name: string;
50
+ lead?: string;
51
+ members: TeamMember[];
52
+ goal?: string;
53
+ createdTs: string;
54
+ }
55
+ export interface Registry {
56
+ version: 1;
57
+ personas: Record<string, PersonaRecord>;
58
+ projects: Record<string, ProjectRecord>;
59
+ collections: Record<string, Collection>;
60
+ teams: Record<string, Team>;
61
+ machines: Record<string, {
62
+ lastSeen: string;
63
+ os: string;
64
+ }>;
65
+ }
66
+ export declare function personaxisHome(): string;
67
+ /** A stable per-machine id so cross-OS instances reconcile without clobbering. */
68
+ export declare function machineId(): string;
69
+ export declare function loadRegistry(): Registry;
70
+ export declare function saveRegistry(reg: Registry): void;
71
+ export declare function globalPersonaDir(slug: string): string;
72
+ export declare function registerPersona(slug: string): PersonaRecord;
73
+ export declare function registerProject(root: string, slugs: string[]): ProjectRecord;
74
+ export declare function createCollection(name: string): Collection;
75
+ export declare function addToCollection(name: string, kind: "persona" | "project", value: string): Collection;
76
+ export declare function createTeam(name: string, lead?: string): Team;
77
+ export declare function addTeamMember(name: string, slug: string, role: string): Team;
78
+ export declare function setTeamGoal(name: string, goal: string): Team;
79
+ export declare function getTeam(name: string): Team | undefined;
80
+ /**
81
+ * Resolve the effective persona path for a project + slug: a project-local
82
+ * overlay (project `.personaxis/personas/<slug>/personaxis.md` or root
83
+ * `.personaxis/personaxis.md`) takes precedence over the global persona.
84
+ * Returns the first existing path, or the global path as the default target.
85
+ */
86
+ export declare function resolveEffectivePersona(projectRoot: string, slug?: string): {
87
+ path: string;
88
+ scope: "project-overlay" | "global" | "none";
89
+ };
90
+ /** The overseer's situational summary of the whole environment. */
91
+ export declare function overseerView(): {
92
+ machine: string;
93
+ personas: number;
94
+ projects: number;
95
+ collections: number;
96
+ teams: number;
97
+ machines: number;
98
+ detail: Registry;
99
+ };