portable-agent-layer 0.32.0 → 0.33.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/assets/skills/presentation/SKILL.md +124 -5
- package/assets/skills/presentation/WORKSHOP.md +128 -0
- package/assets/skills/presentation/theme-base/base.css +113 -0
- package/assets/skills/presentation/theme-base/layouts.css +11 -2
- package/assets/skills/presentation/tools/build.ts +136 -6
- package/assets/skills/presentation/tools/doctor.ts +106 -317
- package/assets/skills/presentation/tools/lib/lint-helpers.ts +150 -0
- package/assets/skills/presentation/tools/lib/lint-rules.ts +744 -0
- package/assets/skills/presentation/tools/lib/lint-types.ts +40 -0
- package/assets/skills/presentation/tools/new-deck.ts +9 -4
- package/assets/skills/presentation/vendor/reveal/plugin/highlight/github-dark.css +118 -0
- package/assets/skills/projects/SKILL.md +111 -0
- package/assets/skills/telos/SKILL.md +4 -1
- package/assets/templates/AGENTS.md.template +28 -7
- package/assets/templates/PAL/ALGORITHM.md +2 -0
- package/assets/templates/PAL/README.md +0 -1
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +1 -1
- package/assets/templates/pal-settings.json +2 -2
- package/package.json +1 -1
- package/src/hooks/UserPromptOrchestrator.ts +3 -1
- package/src/hooks/handlers/auto-graduate.ts +169 -0
- package/src/hooks/handlers/inject-retrieval.ts +50 -0
- package/src/hooks/handlers/project-touch.ts +39 -0
- package/src/hooks/lib/context.ts +9 -8
- package/src/hooks/lib/paths.ts +2 -0
- package/src/hooks/lib/projects.ts +270 -0
- package/src/hooks/lib/retrieval-index.ts +223 -0
- package/src/hooks/lib/retrieval.ts +170 -0
- package/src/hooks/lib/security.ts +2 -0
- package/src/hooks/lib/stop.ts +9 -1
- package/src/hooks/lib/text-similarity.ts +13 -9
- package/src/hooks/lib/wisdom.ts +155 -1
- package/src/tools/agent/project.ts +336 -0
- package/src/tools/self-model.ts +3 -3
- package/assets/templates/PAL/CONTEXT_ROUTING.md +0 -30
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retrieval ranker — score the indexed corpus against a fresh prompt and produce a
|
|
3
|
+
* ≤500-char `<system-reminder>` block of the top-N matching principles.
|
|
4
|
+
*
|
|
5
|
+
* Algorithm: IDF-weighted term overlap with capped tf, sqrt length-norm, age decay,
|
|
6
|
+
* cwd-fingerprint scope boost, confidence threshold.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { basename } from "node:path";
|
|
10
|
+
import type { IndexedDoc, RetrievalIndex } from "./retrieval-index";
|
|
11
|
+
import { extractKeywords } from "./text-similarity";
|
|
12
|
+
|
|
13
|
+
const TF_CAP = 3;
|
|
14
|
+
const TIME_HALF_LIFE_DAYS = 90;
|
|
15
|
+
const SCOPE_BOOST = 1.5;
|
|
16
|
+
const CONFIDENCE_THRESHOLD = 0.18;
|
|
17
|
+
const MAX_MATCHES = 2;
|
|
18
|
+
const MAX_REMINDER_BYTES = 500;
|
|
19
|
+
const PRINCIPLE_TRUNC = 200;
|
|
20
|
+
|
|
21
|
+
export interface ScoredDoc {
|
|
22
|
+
doc: IndexedDoc;
|
|
23
|
+
score: number;
|
|
24
|
+
confidence: number;
|
|
25
|
+
scopeMatch: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function idf(term: string, df: Record<string, number>, N: number): number {
|
|
29
|
+
return Math.log((N + 1) / ((df[term] ?? 0) + 1)) + 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ageDecay(ts: string): number {
|
|
33
|
+
if (!ts) return 1;
|
|
34
|
+
const age = Date.now() - new Date(ts).getTime();
|
|
35
|
+
if (!Number.isFinite(age) || age <= 0) return 1;
|
|
36
|
+
const days = age / 86_400_000;
|
|
37
|
+
return Math.exp(-days / TIME_HALF_LIFE_DAYS);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Raw IDF-weighted overlap score, capped tf, sqrt-length-normalized. */
|
|
41
|
+
export function scoreDoc(
|
|
42
|
+
queryTerms: Set<string>,
|
|
43
|
+
doc: IndexedDoc,
|
|
44
|
+
df: Record<string, number>,
|
|
45
|
+
N: number
|
|
46
|
+
): number {
|
|
47
|
+
if (doc.len === 0) return 0;
|
|
48
|
+
let s = 0;
|
|
49
|
+
for (const t of queryTerms) {
|
|
50
|
+
const tf = doc.tf[t];
|
|
51
|
+
if (!tf) continue;
|
|
52
|
+
s += idf(t, df, N) * Math.min(tf, TF_CAP);
|
|
53
|
+
}
|
|
54
|
+
if (s === 0) return 0;
|
|
55
|
+
return s / Math.sqrt(doc.len);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Upper bound: query treated as a doc with tf=1 per unique term. */
|
|
59
|
+
export function selfScore(
|
|
60
|
+
queryTerms: Set<string>,
|
|
61
|
+
df: Record<string, number>,
|
|
62
|
+
N: number
|
|
63
|
+
): number {
|
|
64
|
+
if (queryTerms.size === 0) return 0;
|
|
65
|
+
let s = 0;
|
|
66
|
+
for (const t of queryTerms) s += idf(t, df, N);
|
|
67
|
+
return s / Math.sqrt(queryTerms.size);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Does any token of the doc's tf map match the cwd basename (lowercased)? */
|
|
71
|
+
function scopeMatches(doc: IndexedDoc, scopeKey: string): boolean {
|
|
72
|
+
if (!scopeKey) return false;
|
|
73
|
+
return doc.tf[scopeKey] !== undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Rank the index against `query`. Returns docs above the confidence threshold,
|
|
77
|
+
* scope-boosted and age-decayed, sorted high-to-low, capped at MAX_MATCHES. */
|
|
78
|
+
export function rank(query: string, index: RetrievalIndex, cwd: string): ScoredDoc[] {
|
|
79
|
+
const queryTerms = extractKeywords(query);
|
|
80
|
+
if (queryTerms.size === 0) return [];
|
|
81
|
+
|
|
82
|
+
const N = Math.max(index.corpusSize, 1);
|
|
83
|
+
const self = selfScore(queryTerms, index.df, N);
|
|
84
|
+
if (self === 0) return [];
|
|
85
|
+
|
|
86
|
+
const scopeKey = basename(cwd)
|
|
87
|
+
.toLowerCase()
|
|
88
|
+
.replace(/[^a-z0-9-]/g, "");
|
|
89
|
+
const scopeTokens = scopeKey ? extractKeywords(scopeKey) : new Set<string>();
|
|
90
|
+
|
|
91
|
+
const scored: ScoredDoc[] = [];
|
|
92
|
+
for (const doc of index.docs) {
|
|
93
|
+
const raw = scoreDoc(queryTerms, doc, index.df, N);
|
|
94
|
+
if (raw === 0) continue;
|
|
95
|
+
const scopeMatch = [...scopeTokens].some((t) => scopeMatches(doc, t));
|
|
96
|
+
const boosted = raw * (scopeMatch ? SCOPE_BOOST : 1) * ageDecay(doc.ts);
|
|
97
|
+
const confidence = boosted / self;
|
|
98
|
+
if (confidence < CONFIDENCE_THRESHOLD) continue;
|
|
99
|
+
scored.push({ doc, score: boosted, confidence, scopeMatch });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
scored.sort((a, b) => b.score - a.score);
|
|
103
|
+
return scored.slice(0, MAX_MATCHES);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function truncate(text: string, max: number): string {
|
|
107
|
+
if (text.length <= max) return text;
|
|
108
|
+
return `${text.slice(0, max - 1).trimEnd()}…`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function formatAgo(ts: string): string {
|
|
112
|
+
if (!ts) return "";
|
|
113
|
+
const age = Date.now() - new Date(ts).getTime();
|
|
114
|
+
if (!Number.isFinite(age) || age <= 0) return "";
|
|
115
|
+
const days = Math.floor(age / 86_400_000);
|
|
116
|
+
if (days < 1) return "today";
|
|
117
|
+
if (days < 30) return `${days}d ago`;
|
|
118
|
+
const months = Math.floor(days / 30);
|
|
119
|
+
return `${months}mo ago`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatLine(s: ScoredDoc): string {
|
|
123
|
+
const tag =
|
|
124
|
+
s.doc.source === "wisdom"
|
|
125
|
+
? `[${s.doc.displayContext}]`
|
|
126
|
+
: s.scopeMatch
|
|
127
|
+
? "[project]"
|
|
128
|
+
: "[global]";
|
|
129
|
+
const text = s.doc.displayPrinciple || s.doc.displayContext || "";
|
|
130
|
+
const principle = truncate(text, PRINCIPLE_TRUNC);
|
|
131
|
+
if (s.doc.source === "wisdom") {
|
|
132
|
+
return `- ${tag} ${principle} (CRYSTAL ${s.doc.rating}%)`;
|
|
133
|
+
}
|
|
134
|
+
const ago = formatAgo(s.doc.ts);
|
|
135
|
+
const meta = ago ? `rating ${s.doc.rating}/10, ${ago}` : `rating ${s.doc.rating}/10`;
|
|
136
|
+
return `- ${tag} ${principle} (${meta})`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Build the `<system-reminder>` block. Drops lowest-ranked lines until ≤500 bytes. */
|
|
140
|
+
export function formatReminder(matches: ScoredDoc[]): string {
|
|
141
|
+
if (matches.length === 0) return "";
|
|
142
|
+
let lines = matches.map(formatLine);
|
|
143
|
+
|
|
144
|
+
let block = render(lines);
|
|
145
|
+
while (block.length > MAX_REMINDER_BYTES && lines.length > 1) {
|
|
146
|
+
lines = lines.slice(0, -1);
|
|
147
|
+
block = render(lines);
|
|
148
|
+
}
|
|
149
|
+
if (block.length > MAX_REMINDER_BYTES) return "";
|
|
150
|
+
return block;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function render(lines: string[]): string {
|
|
154
|
+
return [
|
|
155
|
+
"<system-reminder>",
|
|
156
|
+
"**Relevant prior lessons** (matched on your prompt):",
|
|
157
|
+
...lines,
|
|
158
|
+
"</system-reminder>",
|
|
159
|
+
].join("\n");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** End-to-end retrieval: takes a query + index, returns the formatted reminder string. */
|
|
163
|
+
export function runRetrieval(
|
|
164
|
+
query: string,
|
|
165
|
+
index: RetrievalIndex,
|
|
166
|
+
cwd: string
|
|
167
|
+
): { reminder: string; matches: ScoredDoc[] } {
|
|
168
|
+
const matches = rank(query, index, cwd);
|
|
169
|
+
return { reminder: formatReminder(matches), matches };
|
|
170
|
+
}
|
|
@@ -36,6 +36,7 @@ export const HOOK_MANAGED_FILES = [
|
|
|
36
36
|
"pal-settings.json",
|
|
37
37
|
"skill-index.json",
|
|
38
38
|
"algorithm-reflections.jsonl",
|
|
39
|
+
".retrieval-index.json",
|
|
39
40
|
];
|
|
40
41
|
|
|
41
42
|
/** Hook-managed directories — AI must not write to or delete from these */
|
|
@@ -47,6 +48,7 @@ export const HOOK_MANAGED_DIRS = [
|
|
|
47
48
|
"memory/relationship",
|
|
48
49
|
"memory/wisdom/state",
|
|
49
50
|
"memory/projects",
|
|
51
|
+
"memory/state/progress",
|
|
50
52
|
];
|
|
51
53
|
|
|
52
54
|
/** Escape a string for use in a RegExp */
|
package/src/hooks/lib/stop.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { resolve } from "node:path";
|
|
8
|
+
import { autoGraduate } from "../handlers/auto-graduate";
|
|
8
9
|
import { autoBackup } from "../handlers/backup";
|
|
9
10
|
import { notifyDesktop } from "../handlers/desktop-notify";
|
|
10
11
|
import { captureFailure } from "../handlers/failure";
|
|
12
|
+
import { projectTouch } from "../handlers/project-touch";
|
|
11
13
|
import { checkReflectTrigger } from "../handlers/reflect-trigger";
|
|
12
14
|
import { checkSelfModelTrigger } from "../handlers/self-model-trigger";
|
|
13
15
|
import { captureSessionIntelligence } from "../handlers/session-intelligence";
|
|
@@ -38,7 +40,9 @@ export async function runStopHandlers(
|
|
|
38
40
|
// Cache last assistant response (session-scoped)
|
|
39
41
|
cacheLastResponse(messages, options.lastAssistantMessage, options.sessionId);
|
|
40
42
|
|
|
41
|
-
// Run all handlers concurrently
|
|
43
|
+
// Run all handlers concurrently. Auto-graduate is idempotent (24h TTL +
|
|
44
|
+
// state-dedup + content-dedup) so it's safe to fire on every Stop.
|
|
45
|
+
// project-touch only fires when cwd resolves to an active registered project.
|
|
42
46
|
const results = await Promise.allSettled([
|
|
43
47
|
captureWorkSession(transcript, options.sessionId),
|
|
44
48
|
resetTab(),
|
|
@@ -49,6 +53,8 @@ export async function runStopHandlers(
|
|
|
49
53
|
checkReflectTrigger(),
|
|
50
54
|
checkSelfModelTrigger(),
|
|
51
55
|
runSynthesis(),
|
|
56
|
+
autoGraduate(),
|
|
57
|
+
projectTouch(options.lastAssistantMessage),
|
|
52
58
|
notifyDesktop(options.sessionId),
|
|
53
59
|
]);
|
|
54
60
|
|
|
@@ -62,6 +68,8 @@ export async function runStopHandlers(
|
|
|
62
68
|
"reflect-trigger",
|
|
63
69
|
"self-model-trigger",
|
|
64
70
|
"synthesis",
|
|
71
|
+
"auto-graduate",
|
|
72
|
+
"project-touch",
|
|
65
73
|
"desktop-notify",
|
|
66
74
|
];
|
|
67
75
|
for (let i = 0; i < results.length; i++) {
|
|
@@ -126,16 +126,20 @@ function stem(word: string): string {
|
|
|
126
126
|
return stemOnce(stemOnce(word));
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/** Lowercase → strip punctuation → split → drop stop-words and shorts → stem → drop shorts.
|
|
130
|
+
* Returns the token stream WITH duplicates (needed for term-frequency counting). */
|
|
131
|
+
export function tokenize(text: string): string[] {
|
|
132
|
+
return text
|
|
133
|
+
.toLowerCase()
|
|
134
|
+
.replace(/[^a-z0-9\s-]/g, " ")
|
|
135
|
+
.split(/\s+/)
|
|
136
|
+
.filter((w) => w.length > 2 && !STOP_WORDS.has(w))
|
|
137
|
+
.map((w) => stem(w))
|
|
138
|
+
.filter((w) => w.length > 2);
|
|
139
|
+
}
|
|
140
|
+
|
|
129
141
|
export function extractKeywords(text: string): Set<string> {
|
|
130
|
-
return new Set(
|
|
131
|
-
text
|
|
132
|
-
.toLowerCase()
|
|
133
|
-
.replace(/[^a-z0-9\s-]/g, " ")
|
|
134
|
-
.split(/\s+/)
|
|
135
|
-
.filter((w) => w.length > 2 && !STOP_WORDS.has(w))
|
|
136
|
-
.map((w) => stem(w))
|
|
137
|
-
.filter((w) => w.length > 2)
|
|
138
|
-
);
|
|
142
|
+
return new Set(tokenize(text));
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
/** Dice coefficient on keyword sets. Returns 0-1. */
|
package/src/hooks/lib/wisdom.ts
CHANGED
|
@@ -8,9 +8,163 @@
|
|
|
8
8
|
* not auto-extracted from transcripts.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { resolve } from "node:path";
|
|
13
13
|
import { paths } from "./paths";
|
|
14
|
+
import { similarity } from "./text-similarity";
|
|
15
|
+
|
|
16
|
+
/** Dice-similarity floor for "principle already represented" — matches graduation.ts. */
|
|
17
|
+
const PRINCIPLE_DEDUP_THRESHOLD = 0.3;
|
|
18
|
+
const PRINCIPLES_PLACEHOLDER =
|
|
19
|
+
"*No crystallized principles yet. Observations accumulating.*";
|
|
20
|
+
|
|
21
|
+
function today(): string {
|
|
22
|
+
return new Date().toISOString().slice(0, 10);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function scaffoldFrame(domain: string): string {
|
|
26
|
+
const t = today();
|
|
27
|
+
return `# Frame: ${domain.charAt(0).toUpperCase() + domain.slice(1)}
|
|
28
|
+
|
|
29
|
+
## Meta
|
|
30
|
+
- **Domain:** ${domain}
|
|
31
|
+
- **Observation Count:** 0
|
|
32
|
+
- **Last Updated:** ${t}
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Core Principles
|
|
37
|
+
|
|
38
|
+
${PRINCIPLES_PLACEHOLDER}
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Contextual Rules
|
|
43
|
+
|
|
44
|
+
*None yet.*
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Anti-Patterns
|
|
49
|
+
|
|
50
|
+
*None yet.*
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Evolution Log
|
|
55
|
+
- ${t}: Frame scaffolded by auto-graduate
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Existing CRYSTAL principle texts in the frame content (both v4 heading + legacy bullet). */
|
|
60
|
+
function existingCrystalPrinciples(content: string): string[] {
|
|
61
|
+
const out: string[] = [];
|
|
62
|
+
for (const m of content.matchAll(/^### (.+?) \[CRYSTAL:\s*\d+%\]/gm)) {
|
|
63
|
+
if (m[1]) out.push(m[1].trim());
|
|
64
|
+
}
|
|
65
|
+
for (const line of content.split("\n")) {
|
|
66
|
+
const m = line.match(/^\s*-\s*(.+?)\s*\[CRYSTAL:\s*\d+%\]\s*$/);
|
|
67
|
+
if (m?.[1]) out.push(m[1].trim());
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface PromoteCrystalResult {
|
|
73
|
+
domain: string;
|
|
74
|
+
principle: string;
|
|
75
|
+
confidence: number;
|
|
76
|
+
framePath: string;
|
|
77
|
+
/** "duplicate" → a Dice-similar CRYSTAL line already existed; nothing written. */
|
|
78
|
+
skipped: "duplicate" | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Idempotently promote a graduated principle to a wisdom-frame CRYSTAL line.
|
|
83
|
+
*
|
|
84
|
+
* Content-dedup: if any existing CRYSTAL line in the frame is Dice-similar
|
|
85
|
+
* (≥0.3) to `principle`, the call is a no-op. Combined with the auto-graduate
|
|
86
|
+
* handler's TTL guard and state-dedup, this means N rapid calls with the same
|
|
87
|
+
* input produce ≤1 file write — the property the past failed attempt missed.
|
|
88
|
+
*/
|
|
89
|
+
export function promoteCrystal(
|
|
90
|
+
domain: string,
|
|
91
|
+
principle: string,
|
|
92
|
+
confidence: number
|
|
93
|
+
): PromoteCrystalResult {
|
|
94
|
+
const framesDir = paths.wisdom();
|
|
95
|
+
mkdirSync(framesDir, { recursive: true });
|
|
96
|
+
const framePath = resolve(framesDir, `${domain}.md`);
|
|
97
|
+
|
|
98
|
+
if (!existsSync(framePath)) {
|
|
99
|
+
writeFileSync(framePath, scaffoldFrame(domain));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let content = readFileSync(framePath, "utf-8");
|
|
103
|
+
|
|
104
|
+
for (const existing of existingCrystalPrinciples(content)) {
|
|
105
|
+
if (similarity(principle, existing) >= PRINCIPLE_DEDUP_THRESHOLD) {
|
|
106
|
+
return { domain, principle, confidence, framePath, skipped: "duplicate" };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const newLine = `### ${principle} [CRYSTAL: ${confidence}%]`;
|
|
111
|
+
|
|
112
|
+
if (content.includes(PRINCIPLES_PLACEHOLDER)) {
|
|
113
|
+
content = content.replace(PRINCIPLES_PLACEHOLDER, newLine);
|
|
114
|
+
} else {
|
|
115
|
+
content = content.replace(/(## Core Principles\n+)/, `$1${newLine}\n\n`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
content = content.replace(/(\*\*Last Updated:\*\*\s*)\S+/, `$1${today()}`);
|
|
119
|
+
const evolutionEntry = `- ${today()}: Auto-promoted to CRYSTAL ${confidence}% — ${principle}`;
|
|
120
|
+
content = content.replace(/(## Evolution Log\n)/, `$1${evolutionEntry}\n`);
|
|
121
|
+
|
|
122
|
+
writeFileSync(framePath, content);
|
|
123
|
+
return { domain, principle, confidence, framePath, skipped: null };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface FrameDoc {
|
|
127
|
+
domain: string;
|
|
128
|
+
principle: string;
|
|
129
|
+
body: string;
|
|
130
|
+
confidence: number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Extract every CRYSTAL principle as a structured doc (domain + principle + surrounding body + confidence).
|
|
134
|
+
* Body is the first ~600 chars of the frame for ranking context.
|
|
135
|
+
* Used by the retrieval indexer; readFramePrinciples remains for SessionStart formatting. */
|
|
136
|
+
export function readFramesForRetrieval(): FrameDoc[] {
|
|
137
|
+
const framesDir = paths.wisdom();
|
|
138
|
+
const docs: FrameDoc[] = [];
|
|
139
|
+
|
|
140
|
+
if (!existsSync(framesDir)) return docs;
|
|
141
|
+
|
|
142
|
+
for (const file of readdirSync(framesDir).filter((f) => f.endsWith(".md"))) {
|
|
143
|
+
const domain = file.replace(".md", "");
|
|
144
|
+
const content = readFileSync(resolve(framesDir, file), "utf-8");
|
|
145
|
+
const body = content.slice(0, 600);
|
|
146
|
+
|
|
147
|
+
for (const match of content.matchAll(/^### (.+?) \[CRYSTAL:\s*(\d+)%\]/gm)) {
|
|
148
|
+
const name = match[1]?.trim();
|
|
149
|
+
const pct = parseInt(match[2] ?? "", 10);
|
|
150
|
+
if (name && Number.isFinite(pct) && pct >= 85) {
|
|
151
|
+
docs.push({ domain, principle: name, body, confidence: pct });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const line of content.split("\n")) {
|
|
156
|
+
const m = line.match(/^\s*-\s*(.+?)\s*\[CRYSTAL:\s*(\d+)%\]\s*$/);
|
|
157
|
+
if (!m) continue;
|
|
158
|
+
const name = m[1]?.trim();
|
|
159
|
+
const pct = parseInt(m[2] ?? "", 10);
|
|
160
|
+
if (name && Number.isFinite(pct) && pct >= 85) {
|
|
161
|
+
docs.push({ domain, principle: name, body, confidence: pct });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return docs;
|
|
167
|
+
}
|
|
14
168
|
|
|
15
169
|
/** Extract CRYSTAL principles (≥85% confidence) from all frame files */
|
|
16
170
|
export function readFramePrinciples(): string[] {
|