@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.
- package/LICENSE +21 -0
- package/dist/agent.d.ts +105 -0
- package/dist/agent.js +429 -0
- package/dist/appraisal.d.ts +196 -0
- package/dist/appraisal.js +159 -0
- package/dist/approval.d.ts +42 -0
- package/dist/approval.js +71 -0
- package/dist/blackboard.d.ts +86 -0
- package/dist/blackboard.js +139 -0
- package/dist/compile/assemble.d.ts +44 -0
- package/dist/compile/assemble.js +338 -0
- package/dist/compile/dist.d.ts +30 -0
- package/dist/compile/dist.js +66 -0
- package/dist/compile/faithfulness.d.ts +47 -0
- package/dist/compile/faithfulness.js +112 -0
- package/dist/compile/index.d.ts +10 -0
- package/dist/compile/index.js +10 -0
- package/dist/compile/targets.d.ts +62 -0
- package/dist/compile/targets.js +114 -0
- package/dist/config-layers.d.ts +42 -0
- package/dist/config-layers.js +59 -0
- package/dist/config-scan.d.ts +30 -0
- package/dist/config-scan.js +118 -0
- package/dist/context.d.ts +67 -0
- package/dist/context.js +178 -0
- package/dist/envelopes.d.ts +48 -0
- package/dist/envelopes.js +131 -0
- package/dist/events.d.ts +135 -0
- package/dist/events.js +19 -0
- package/dist/evolution-view.d.ts +43 -0
- package/dist/evolution-view.js +67 -0
- package/dist/generated/version.d.ts +1 -0
- package/dist/generated/version.js +2 -0
- package/dist/governance.d.ts +108 -0
- package/dist/governance.js +204 -0
- package/dist/heuristic-appraiser.d.ts +13 -0
- package/dist/heuristic-appraiser.js +48 -0
- package/dist/hooks.d.ts +52 -0
- package/dist/hooks.js +122 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +53 -0
- package/dist/injection.d.ts +48 -0
- package/dist/injection.js +141 -0
- package/dist/live-sync.d.ts +60 -0
- package/dist/live-sync.js +85 -0
- package/dist/llm-appraiser.d.ts +31 -0
- package/dist/llm-appraiser.js +143 -0
- package/dist/lock.d.ts +26 -0
- package/dist/lock.js +99 -0
- package/dist/loop.d.ts +52 -0
- package/dist/loop.js +253 -0
- package/dist/memory-kinds.d.ts +67 -0
- package/dist/memory-kinds.js +106 -0
- package/dist/memory.d.ts +123 -0
- package/dist/memory.js +228 -0
- package/dist/model-config.d.ts +60 -0
- package/dist/model-config.js +102 -0
- package/dist/persona-theme.d.ts +57 -0
- package/dist/persona-theme.js +183 -0
- package/dist/persona.d.ts +88 -0
- package/dist/persona.js +93 -0
- package/dist/ports/index.d.ts +81 -0
- package/dist/ports/index.js +50 -0
- package/dist/provenance.d.ts +63 -0
- package/dist/provenance.js +132 -0
- package/dist/recompile-marker.d.ts +19 -0
- package/dist/recompile-marker.js +36 -0
- package/dist/registry.d.ts +99 -0
- package/dist/registry.js +168 -0
- package/dist/responder.d.ts +47 -0
- package/dist/responder.js +78 -0
- package/dist/sandbox.d.ts +120 -0
- package/dist/sandbox.js +265 -0
- package/dist/self-evolution.d.ts +148 -0
- package/dist/self-evolution.js +364 -0
- package/dist/session-writer.d.ts +52 -0
- package/dist/session-writer.js +104 -0
- package/dist/sessions.d.ts +96 -0
- package/dist/sessions.js +179 -0
- package/dist/sigil.d.ts +41 -0
- package/dist/sigil.js +92 -0
- package/dist/skill-lifecycle.d.ts +74 -0
- package/dist/skill-lifecycle.js +126 -0
- package/dist/skill-review.d.ts +25 -0
- package/dist/skill-review.js +67 -0
- package/dist/spec-edit.d.ts +30 -0
- package/dist/spec-edit.js +137 -0
- package/dist/state-engine.d.ts +40 -0
- package/dist/state-engine.js +50 -0
- package/dist/state-rebuild.d.ts +48 -0
- package/dist/state-rebuild.js +60 -0
- package/dist/sync.d.ts +32 -0
- package/dist/sync.js +95 -0
- package/dist/tool-calling.d.ts +59 -0
- package/dist/tool-calling.js +168 -0
- package/dist/tool-repair.d.ts +20 -0
- package/dist/tool-repair.js +119 -0
- package/dist/tools/exec.d.ts +46 -0
- package/dist/tools/exec.js +132 -0
- package/dist/tools/registry.d.ts +38 -0
- package/dist/tools/registry.js +162 -0
- package/dist/trace.d.ts +63 -0
- package/dist/trace.js +162 -0
- package/dist/verification.d.ts +65 -0
- package/dist/verification.js +190 -0
- 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;
|
package/dist/persona.js
ADDED
|
@@ -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
|
+
};
|