praana 0.5.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 (204) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/bin/praana.js +17 -0
  4. package/bin/pran.js +17 -0
  5. package/dist/app-banner.d.ts +11 -0
  6. package/dist/app-banner.js +161 -0
  7. package/dist/app-controller.d.ts +44 -0
  8. package/dist/app-controller.js +143 -0
  9. package/dist/app-identity.d.ts +18 -0
  10. package/dist/app-identity.js +52 -0
  11. package/dist/auto-compact.d.ts +16 -0
  12. package/dist/auto-compact.js +101 -0
  13. package/dist/cli-args.d.ts +14 -0
  14. package/dist/cli-args.js +69 -0
  15. package/dist/compile-classic.d.ts +21 -0
  16. package/dist/compile-classic.js +106 -0
  17. package/dist/compiler.d.ts +75 -0
  18. package/dist/compiler.js +406 -0
  19. package/dist/config.d.ts +3 -0
  20. package/dist/config.js +433 -0
  21. package/dist/context-engine/activity-log.d.ts +9 -0
  22. package/dist/context-engine/activity-log.js +109 -0
  23. package/dist/context-engine/artifact-store.d.ts +32 -0
  24. package/dist/context-engine/artifact-store.js +272 -0
  25. package/dist/context-engine/bm25.d.ts +3 -0
  26. package/dist/context-engine/bm25.js +32 -0
  27. package/dist/context-engine/checkpoint.d.ts +34 -0
  28. package/dist/context-engine/checkpoint.js +430 -0
  29. package/dist/context-engine/classify.d.ts +3 -0
  30. package/dist/context-engine/classify.js +60 -0
  31. package/dist/context-engine/db.d.ts +73 -0
  32. package/dist/context-engine/db.js +505 -0
  33. package/dist/context-engine/distiller.d.ts +30 -0
  34. package/dist/context-engine/distiller.js +67 -0
  35. package/dist/context-engine/engine-compiler.d.ts +23 -0
  36. package/dist/context-engine/engine-compiler.js +297 -0
  37. package/dist/context-engine/error-tracker.d.ts +21 -0
  38. package/dist/context-engine/error-tracker.js +74 -0
  39. package/dist/context-engine/event-lineage.d.ts +26 -0
  40. package/dist/context-engine/event-lineage.js +120 -0
  41. package/dist/context-engine/extraction.d.ts +26 -0
  42. package/dist/context-engine/extraction.js +83 -0
  43. package/dist/context-engine/index.d.ts +82 -0
  44. package/dist/context-engine/index.js +238 -0
  45. package/dist/context-engine/scoring.d.ts +13 -0
  46. package/dist/context-engine/scoring.js +47 -0
  47. package/dist/context-engine/state-snapshot.d.ts +8 -0
  48. package/dist/context-engine/state-snapshot.js +50 -0
  49. package/dist/context-engine/summarize.d.ts +6 -0
  50. package/dist/context-engine/summarize.js +32 -0
  51. package/dist/context-engine/telemetry.d.ts +25 -0
  52. package/dist/context-engine/telemetry.js +64 -0
  53. package/dist/context-engine/turn-digest.d.ts +50 -0
  54. package/dist/context-engine/turn-digest.js +250 -0
  55. package/dist/context-engine/turn-ledger.d.ts +18 -0
  56. package/dist/context-engine/turn-ledger.js +184 -0
  57. package/dist/context-engine/turn-recorder.d.ts +24 -0
  58. package/dist/context-engine/turn-recorder.js +88 -0
  59. package/dist/context-engine/types.d.ts +201 -0
  60. package/dist/context-engine/types.js +4 -0
  61. package/dist/context-pressure.d.ts +19 -0
  62. package/dist/context-pressure.js +36 -0
  63. package/dist/distillers/generic.d.ts +14 -0
  64. package/dist/distillers/generic.js +93 -0
  65. package/dist/distillers/git-diff.d.ts +8 -0
  66. package/dist/distillers/git-diff.js +119 -0
  67. package/dist/distillers/index.d.ts +2 -0
  68. package/dist/distillers/index.js +16 -0
  69. package/dist/distillers/npm-test.d.ts +8 -0
  70. package/dist/distillers/npm-test.js +50 -0
  71. package/dist/distillers/rg-results.d.ts +8 -0
  72. package/dist/distillers/rg-results.js +28 -0
  73. package/dist/distillers/tsc-errors.d.ts +8 -0
  74. package/dist/distillers/tsc-errors.js +52 -0
  75. package/dist/event-log.d.ts +56 -0
  76. package/dist/event-log.js +214 -0
  77. package/dist/llm.d.ts +29 -0
  78. package/dist/llm.js +155 -0
  79. package/dist/logger.d.ts +94 -0
  80. package/dist/logger.js +287 -0
  81. package/dist/main.d.ts +1 -0
  82. package/dist/main.js +54 -0
  83. package/dist/memory/confidence.d.ts +7 -0
  84. package/dist/memory/confidence.js +37 -0
  85. package/dist/memory/consolidation.d.ts +26 -0
  86. package/dist/memory/consolidation.js +166 -0
  87. package/dist/memory/db.d.ts +40 -0
  88. package/dist/memory/db.js +283 -0
  89. package/dist/memory/dedup.d.ts +6 -0
  90. package/dist/memory/dedup.js +50 -0
  91. package/dist/memory/embedder-factory.d.ts +3 -0
  92. package/dist/memory/embedder-factory.js +81 -0
  93. package/dist/memory/embeddings.d.ts +15 -0
  94. package/dist/memory/embeddings.js +67 -0
  95. package/dist/memory/index.d.ts +9 -0
  96. package/dist/memory/index.js +11 -0
  97. package/dist/memory/ollama-summarizer.d.ts +19 -0
  98. package/dist/memory/ollama-summarizer.js +72 -0
  99. package/dist/memory/openai-summarizer.d.ts +21 -0
  100. package/dist/memory/openai-summarizer.js +51 -0
  101. package/dist/memory/store.d.ts +61 -0
  102. package/dist/memory/store.js +502 -0
  103. package/dist/memory/summarizer-factory.d.ts +3 -0
  104. package/dist/memory/summarizer-factory.js +69 -0
  105. package/dist/memory/summarizer.d.ts +4 -0
  106. package/dist/memory/summarizer.js +112 -0
  107. package/dist/memory/types.d.ts +87 -0
  108. package/dist/memory/types.js +17 -0
  109. package/dist/model-context.d.ts +15 -0
  110. package/dist/model-context.js +212 -0
  111. package/dist/project-detector.d.ts +37 -0
  112. package/dist/project-detector.js +604 -0
  113. package/dist/render.d.ts +15 -0
  114. package/dist/render.js +46 -0
  115. package/dist/session.d.ts +118 -0
  116. package/dist/session.js +809 -0
  117. package/dist/skills/index.d.ts +69 -0
  118. package/dist/skills/index.js +885 -0
  119. package/dist/skills/types.d.ts +93 -0
  120. package/dist/skills/types.js +8 -0
  121. package/dist/slash-commands.d.ts +14 -0
  122. package/dist/slash-commands.js +301 -0
  123. package/dist/state-graph.d.ts +38 -0
  124. package/dist/state-graph.js +255 -0
  125. package/dist/status-bar.d.ts +54 -0
  126. package/dist/status-bar.js +184 -0
  127. package/dist/thinking-display.d.ts +21 -0
  128. package/dist/thinking-display.js +37 -0
  129. package/dist/tool-summary.d.ts +4 -0
  130. package/dist/tool-summary.js +67 -0
  131. package/dist/tools/index.d.ts +925 -0
  132. package/dist/tools/index.js +86 -0
  133. package/dist/tools/knowledge.d.ts +140 -0
  134. package/dist/tools/knowledge.js +260 -0
  135. package/dist/tools/memory.d.ts +39 -0
  136. package/dist/tools/memory.js +300 -0
  137. package/dist/tools/search-code.d.ts +134 -0
  138. package/dist/tools/search-code.js +390 -0
  139. package/dist/tools/system.d.ts +16 -0
  140. package/dist/tools/system.js +499 -0
  141. package/dist/tools/tool-def.d.ts +6 -0
  142. package/dist/tools/tool-def.js +3 -0
  143. package/dist/turn-control.d.ts +51 -0
  144. package/dist/turn-control.js +210 -0
  145. package/dist/turn.d.ts +20 -0
  146. package/dist/turn.js +624 -0
  147. package/dist/types.d.ts +233 -0
  148. package/dist/types.js +4 -0
  149. package/dist/ui/readline-ui.d.ts +2 -0
  150. package/dist/ui/readline-ui.js +176 -0
  151. package/dist/ui/tui/app.d.ts +13 -0
  152. package/dist/ui/tui/app.js +270 -0
  153. package/dist/ui/tui/busy-indicator.d.ts +2 -0
  154. package/dist/ui/tui/busy-indicator.js +13 -0
  155. package/dist/ui/tui/components/gutter-rule.d.ts +5 -0
  156. package/dist/ui/tui/components/gutter-rule.js +9 -0
  157. package/dist/ui/tui/components/inline-tool-row.d.ts +10 -0
  158. package/dist/ui/tui/components/inline-tool-row.js +8 -0
  159. package/dist/ui/tui/components/prompt-input.d.ts +20 -0
  160. package/dist/ui/tui/components/prompt-input.js +120 -0
  161. package/dist/ui/tui/components/system-line.d.ts +5 -0
  162. package/dist/ui/tui/components/system-line.js +6 -0
  163. package/dist/ui/tui/components/thinking-block.d.ts +11 -0
  164. package/dist/ui/tui/components/thinking-block.js +31 -0
  165. package/dist/ui/tui/components/toast-line.d.ts +4 -0
  166. package/dist/ui/tui/components/toast-line.js +8 -0
  167. package/dist/ui/tui/components/tool-result-line.d.ts +5 -0
  168. package/dist/ui/tui/components/tool-result-line.js +6 -0
  169. package/dist/ui/tui/components/turn-footer.d.ts +5 -0
  170. package/dist/ui/tui/components/turn-footer.js +7 -0
  171. package/dist/ui/tui/components/user-block.d.ts +6 -0
  172. package/dist/ui/tui/components/user-block.js +6 -0
  173. package/dist/ui/tui/logo-banner.d.ts +5 -0
  174. package/dist/ui/tui/logo-banner.js +8 -0
  175. package/dist/ui/tui/markdown-render.d.ts +16 -0
  176. package/dist/ui/tui/markdown-render.js +218 -0
  177. package/dist/ui/tui/palette.d.ts +12 -0
  178. package/dist/ui/tui/palette.js +13 -0
  179. package/dist/ui/tui/reasoning-summary.d.ts +12 -0
  180. package/dist/ui/tui/reasoning-summary.js +27 -0
  181. package/dist/ui/tui/reducer.d.ts +92 -0
  182. package/dist/ui/tui/reducer.js +260 -0
  183. package/dist/ui/tui/run.d.ts +3 -0
  184. package/dist/ui/tui/run.js +40 -0
  185. package/dist/ui/tui/sink.d.ts +4 -0
  186. package/dist/ui/tui/sink.js +89 -0
  187. package/dist/ui/tui/status-bar-view.d.ts +5 -0
  188. package/dist/ui/tui/status-bar-view.js +44 -0
  189. package/dist/ui/tui/terminal-height.d.ts +12 -0
  190. package/dist/ui/tui/terminal-height.js +20 -0
  191. package/dist/ui/tui/terminal-width.d.ts +2 -0
  192. package/dist/ui/tui/terminal-width.js +5 -0
  193. package/dist/ui/tui/tool-display.d.ts +23 -0
  194. package/dist/ui/tui/tool-display.js +217 -0
  195. package/dist/ui/tui/transcript-line.d.ts +12 -0
  196. package/dist/ui/tui/transcript-line.js +43 -0
  197. package/dist/ui/tui/transcript-replay.d.ts +12 -0
  198. package/dist/ui/tui/transcript-replay.js +117 -0
  199. package/dist/ui-events.d.ts +39 -0
  200. package/dist/ui-events.js +33 -0
  201. package/dist/ui.d.ts +77 -0
  202. package/dist/ui.js +179 -0
  203. package/package.json +73 -0
  204. package/praana.config.example.toml +231 -0
@@ -0,0 +1,885 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, dirname } from "node:path";
4
+ import { execSync } from "node:child_process";
5
+ import yaml from "js-yaml";
6
+ import { getAppLogger } from "../logger.js";
7
+ // ========================================================================
8
+ // Helpers
9
+ // ========================================================================
10
+ function findGitRoot(cwd) {
11
+ try {
12
+ return execSync("git rev-parse --show-toplevel", {
13
+ cwd,
14
+ encoding: "utf-8",
15
+ stdio: ["ignore", "pipe", "ignore"],
16
+ }).trim();
17
+ }
18
+ catch {
19
+ return cwd;
20
+ }
21
+ }
22
+ function expandHome(p) {
23
+ return p.startsWith("~/") ? p.replace(/^~\//, `${homedir()}/`) : p;
24
+ }
25
+ function isDirectory(path) {
26
+ try {
27
+ return statSync(path).isDirectory();
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ const SKIP_ALLOWLIST = new Set([".agents", ".aria", ".praana", ".cursor", ".claude"]);
34
+ function shouldSkipDir(dirName) {
35
+ if (dirName === ".git" || dirName === "node_modules")
36
+ return true;
37
+ if (dirName.startsWith(".") && !SKIP_ALLOWLIST.has(dirName))
38
+ return true;
39
+ return false;
40
+ }
41
+ // ========================================================================
42
+ // SKILL.md Parsing
43
+ // ========================================================================
44
+ export function parseSkillMdContent(content, filePath) {
45
+ const trimmed = content.trim();
46
+ if (!trimmed.startsWith("---"))
47
+ return null;
48
+ const endIdx = trimmed.indexOf("---", 3);
49
+ if (endIdx === -1)
50
+ return null;
51
+ const yamlBlock = trimmed.slice(3, endIdx).trim();
52
+ const body = trimmed.slice(endIdx + 3).trim();
53
+ let parsed;
54
+ try {
55
+ parsed = yaml.load(yamlBlock);
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ if (!parsed || typeof parsed !== "object")
61
+ return null;
62
+ const name = String(parsed.name ?? "");
63
+ const description = String(parsed.description ?? "");
64
+ if (!name || !description)
65
+ return null;
66
+ const metadata = {
67
+ name,
68
+ description,
69
+ license: parsed.license ? String(parsed.license) : undefined,
70
+ compatibility: parsed.compatibility ? String(parsed.compatibility) : undefined,
71
+ metadata: parsed.metadata,
72
+ allowedTools: parsed["allowed-tools"] ? String(parsed["allowed-tools"]) : undefined,
73
+ };
74
+ return {
75
+ name,
76
+ description,
77
+ location: filePath,
78
+ directory: dirname(filePath),
79
+ body,
80
+ metadata,
81
+ };
82
+ }
83
+ export function parseSkillMdFile(filePath) {
84
+ try {
85
+ const content = readFileSync(filePath, "utf-8");
86
+ return parseSkillMdContent(content, filePath);
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ // ========================================================================
93
+ // skills-meta.json Loading
94
+ // ========================================================================
95
+ function loadSkillsMeta(path) {
96
+ try {
97
+ if (!existsSync(path))
98
+ return {};
99
+ const raw = readFileSync(path, "utf-8");
100
+ return JSON.parse(raw);
101
+ }
102
+ catch {
103
+ return {};
104
+ }
105
+ }
106
+ function getSkillsMetaPaths(cwd) {
107
+ const gitRoot = findGitRoot(cwd);
108
+ const home = homedir();
109
+ return [
110
+ join(gitRoot, ".praana", "skills-meta.json"),
111
+ join(gitRoot, ".aria", "skills-meta.json"),
112
+ expandHome("~/.praana/skills-meta.json"),
113
+ expandHome("~/.aria/skills-meta.json"),
114
+ ];
115
+ }
116
+ export function loadMergedSkillsMeta(cwd) {
117
+ let merged = {};
118
+ for (const path of getSkillsMetaPaths(cwd)) {
119
+ merged = { ...merged, ...loadSkillsMeta(path) };
120
+ }
121
+ return merged;
122
+ }
123
+ // ========================================================================
124
+ // Discovery
125
+ // ========================================================================
126
+ function getSkillSearchPaths(cwd) {
127
+ const gitRoot = findGitRoot(cwd);
128
+ const home = homedir();
129
+ const projectPaths = [
130
+ join(gitRoot, ".agents", "skills"),
131
+ join(gitRoot, ".praana", "skills"),
132
+ join(gitRoot, ".aria", "skills"),
133
+ join(gitRoot, ".cursor", "skills"),
134
+ join(gitRoot, "skills"),
135
+ ];
136
+ const userPaths = [
137
+ join(home, ".agents", "skills"),
138
+ join(home, ".praana", "skills"),
139
+ join(home, ".aria", "skills"),
140
+ join(home, ".claude", "skills"),
141
+ ];
142
+ return [...projectPaths, ...userPaths];
143
+ }
144
+ function scanSkillsDir(skillsDir, maxDepth) {
145
+ if (!existsSync(skillsDir))
146
+ return [];
147
+ const results = [];
148
+ function scan(dir, depth) {
149
+ if (depth > maxDepth)
150
+ return;
151
+ let entries;
152
+ try {
153
+ entries = readdirSync(dir);
154
+ }
155
+ catch {
156
+ return;
157
+ }
158
+ for (const entry of entries) {
159
+ if (shouldSkipDir(entry))
160
+ continue;
161
+ const fullPath = join(dir, entry);
162
+ const skillFile = join(fullPath, "SKILL.md");
163
+ if (isDirectory(fullPath) && existsSync(skillFile)) {
164
+ const skill = parseSkillMdFile(skillFile);
165
+ if (skill) {
166
+ if (skill.name !== entry) {
167
+ getAppLogger().child("skills").warn(`Name mismatch: "${skill.name}" in ${skillFile}, directory is "${entry}"`);
168
+ }
169
+ results.push(skill);
170
+ }
171
+ continue;
172
+ }
173
+ if (entry.endsWith(".md") && !isDirectory(fullPath)) {
174
+ const skill = parseSkillMdFile(fullPath);
175
+ if (skill)
176
+ results.push(skill);
177
+ continue;
178
+ }
179
+ if (isDirectory(fullPath)) {
180
+ scan(fullPath, depth + 1);
181
+ }
182
+ }
183
+ }
184
+ scan(skillsDir, 0);
185
+ return results;
186
+ }
187
+ export function discoverSkills(cwd, maxDepth = 6, _paths) {
188
+ if (_paths) {
189
+ const merged = new Map();
190
+ for (const dir of _paths) {
191
+ for (const skill of scanSkillsDir(dir, maxDepth)) {
192
+ merged.set(skill.name, skill);
193
+ }
194
+ }
195
+ return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
196
+ }
197
+ const paths = getSkillSearchPaths(cwd);
198
+ const projectPaths = paths.slice(0, 4);
199
+ const userPaths = paths.slice(4);
200
+ const projectSkills = new Map();
201
+ const userSkills = new Map();
202
+ for (const dir of projectPaths) {
203
+ for (const skill of scanSkillsDir(dir, maxDepth)) {
204
+ if (!projectSkills.has(skill.name))
205
+ projectSkills.set(skill.name, skill);
206
+ }
207
+ }
208
+ for (const dir of userPaths) {
209
+ for (const skill of scanSkillsDir(dir, maxDepth)) {
210
+ if (!userSkills.has(skill.name))
211
+ userSkills.set(skill.name, skill);
212
+ }
213
+ }
214
+ const merged = new Map(userSkills);
215
+ for (const [name, skill] of projectSkills) {
216
+ if (merged.has(name)) {
217
+ getAppLogger().child("skills").warn(`"${name}" from project overrides user-level skill`);
218
+ }
219
+ merged.set(name, skill);
220
+ }
221
+ return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
222
+ }
223
+ /** Metadata-only skill catalog for classic mode (no residency or BM25). */
224
+ export function buildSkillMetadataCatalog(records) {
225
+ if (records.length === 0)
226
+ return "";
227
+ const lines = [
228
+ "## Available Skills",
229
+ "",
230
+ "Read a skill with read_file when it is relevant:",
231
+ "",
232
+ ];
233
+ for (const skill of records) {
234
+ lines.push(`- **${skill.name}**: ${skill.description} (\`${skill.location}\`)`);
235
+ }
236
+ return lines.join("\n");
237
+ }
238
+ // ========================================================================
239
+ // Section Boundary Detection
240
+ // ========================================================================
241
+ function detectSectionRanges(body, ariaSections) {
242
+ if (!body)
243
+ return undefined;
244
+ const ranges = {};
245
+ const lines = body.split("\n");
246
+ let hasAny = false;
247
+ // Use aria.skill.json section headings if provided, else auto-detect
248
+ const sectionDefs = ariaSections ?? {
249
+ planner: ["## Planner"],
250
+ execution: ["## Execution"],
251
+ recovery: ["## Recovery"],
252
+ examples: ["## Examples"],
253
+ };
254
+ for (const [section, headings] of Object.entries(sectionDefs)) {
255
+ // Find the first heading match for this section
256
+ let startIdx = -1;
257
+ for (let i = 0; i < lines.length; i++) {
258
+ for (const h of headings) {
259
+ if (lines[i].trim().startsWith(h)) {
260
+ startIdx = i;
261
+ break;
262
+ }
263
+ }
264
+ if (startIdx >= 0)
265
+ break;
266
+ }
267
+ if (startIdx < 0)
268
+ continue;
269
+ // Find the next heading or end of body
270
+ let endIdx = lines.length;
271
+ for (let i = startIdx + 1; i < lines.length; i++) {
272
+ const line = lines[i].trim();
273
+ if (line.startsWith("## ")) {
274
+ endIdx = i;
275
+ break;
276
+ }
277
+ }
278
+ ranges[section] = { start: startIdx, end: endIdx };
279
+ hasAny = true;
280
+ }
281
+ return hasAny ? ranges : undefined;
282
+ }
283
+ function extractSection(body, range) {
284
+ if (!range || !body)
285
+ return "";
286
+ const lines = body.split("\n");
287
+ return lines.slice(range.start, range.end).join("\n").trim();
288
+ }
289
+ // ========================================================================
290
+ // BM25 Matcher
291
+ // ========================================================================
292
+ // Default synonym map for V1
293
+ const DEFAULT_SYNONYMS = {
294
+ deploy: ["launch", "release", "rollout", "publish"],
295
+ database: ["db", "postgres", "mysql", "sql", "rds", "dynamodb"],
296
+ container: ["docker", "ecs", "kubernetes", "k8s", "pod"],
297
+ aws: ["amazon", "ec2", "s3", "lambda", "cloud"],
298
+ test: ["testing", "spec", "assert", "verify", "check"],
299
+ build: ["compile", "bundle", "package", "construct"],
300
+ error: ["error", "failure", "bug", "issue", "crash", "exception"],
301
+ fix: ["fix", "repair", "patch", "resolve", "correct"],
302
+ code: ["code", "source", "implementation", "program"],
303
+ review: ["review", "audit", "inspect", "check"],
304
+ config: ["configuration", "setup", "settings", "options"],
305
+ monitor: ["monitoring", "observe", "watch", "track", "metrics"],
306
+ auth: ["authentication", "login", "oauth", "sso", "identity"],
307
+ api: ["rest", "graphql", "endpoint", "service", "http"],
308
+ };
309
+ function tokenize(text) {
310
+ return text
311
+ .toLowerCase()
312
+ .split(/[^a-z0-9]+/)
313
+ .filter((t) => t.length > 1);
314
+ }
315
+ function expandTokens(tokens, synonymMap) {
316
+ const expanded = new Set(tokens);
317
+ for (const t of tokens) {
318
+ const syns = synonymMap[t];
319
+ if (syns)
320
+ for (const s of syns)
321
+ expanded.add(s);
322
+ }
323
+ return [...expanded];
324
+ }
325
+ export function buildBM25Index(skills, meta) {
326
+ return skills.map((s) => {
327
+ const aria = meta[s.name] ?? {};
328
+ const tags = aria.tags ?? [];
329
+ const trigger = aria.trigger ?? "";
330
+ const synonyms = aria.synonyms ?? [];
331
+ // Build search text from name, description, tags, trigger
332
+ const searchParts = [s.name, s.description, ...tags, trigger, ...synonyms];
333
+ const searchText = searchParts.filter(Boolean).join(" ");
334
+ const budgetConfig = aria.budget ?? {};
335
+ const sectionMapping = aria.sections;
336
+ return {
337
+ id: s.name,
338
+ name: s.name,
339
+ description: s.description,
340
+ tags,
341
+ trigger,
342
+ synonyms,
343
+ neighbors: aria.neighbors ?? [],
344
+ searchText,
345
+ sectionRanges: detectSectionRanges(s.body, sectionMapping),
346
+ budgetPriority: budgetConfig.priority ?? "normal",
347
+ maxTokens: budgetConfig.max_tokens ?? 2000,
348
+ };
349
+ });
350
+ }
351
+ /** Score a single query against a document using BM25 */
352
+ function bm25Score(queryTokens, docTokens, avgDocLen, totalDocs, docFreq) {
353
+ const k1 = 1.5;
354
+ const b = 0.75;
355
+ const docLen = docTokens.length;
356
+ // Count term frequencies in this document
357
+ const tf = new Map();
358
+ for (const t of docTokens)
359
+ tf.set(t, (tf.get(t) ?? 0) + 1);
360
+ let score = 0;
361
+ for (const qt of queryTokens) {
362
+ const freq = tf.get(qt) ?? 0;
363
+ if (freq === 0)
364
+ continue;
365
+ const df = docFreq.get(qt) ?? 1;
366
+ const idf = Math.log(1 + (totalDocs - df + 0.5) / (df + 0.5));
367
+ const numerator = freq * (k1 + 1);
368
+ const denominator = freq + k1 * (1 - b + b * (docLen / avgDocLen));
369
+ score += idf * (numerator / denominator);
370
+ }
371
+ return score;
372
+ }
373
+ /**
374
+ * Rank skills by relevance to user input using BM25 + synonym expansion + neighbor boost.
375
+ */
376
+ export function rankSkills(index, userInput, hotSkillIds, synonymMap) {
377
+ if (index.length === 0 || !userInput.trim())
378
+ return [];
379
+ const syns = synonymMap ?? DEFAULT_SYNONYMS;
380
+ const queryTokens = expandTokens(tokenize(userInput), syns);
381
+ if (queryTokens.length === 0)
382
+ return [];
383
+ // Build document frequency map across corpus
384
+ const docFreq = new Map();
385
+ const docTokenLists = [];
386
+ for (const entry of index) {
387
+ const tokens = tokenize(entry.searchText);
388
+ docTokenLists.push(tokens);
389
+ const unique = new Set(tokens);
390
+ for (const t of unique)
391
+ docFreq.set(t, (docFreq.get(t) ?? 0) + 1);
392
+ }
393
+ const totalDocs = index.length;
394
+ const avgDocLen = docTokenLists.reduce((sum, t) => sum + t.length, 0) / Math.max(1, totalDocs);
395
+ const results = [];
396
+ for (let i = 0; i < index.length; i++) {
397
+ const entry = index[i];
398
+ const docTokens = docTokenLists[i];
399
+ let score = bm25Score(queryTokens, docTokens, avgDocLen, totalDocs, docFreq);
400
+ // Keyword score bonus (0.5 weight): fraction of unique doc tokens that match query
401
+ const querySet = new Set(queryTokens);
402
+ const docSet = new Set(docTokens);
403
+ const overlap = [...querySet].filter((t) => docSet.has(t)).length;
404
+ const keywordScore = docSet.size > 0 ? overlap / docSet.size : 0;
405
+ score = score * 0.3 + keywordScore * 0.5;
406
+ // Name match bonus: if the skill name appears in the query, add +0.25
407
+ const nameTokens = tokenize(entry.name);
408
+ const nameMatch = nameTokens.some((nt) => querySet.has(nt));
409
+ if (nameMatch)
410
+ score += 0.25;
411
+ // Exact skill invocation should load the skill, even when the corpus is
412
+ // tiny and BM25/keyword scoring would otherwise leave it WARM.
413
+ if (userInput.trim().toLowerCase() === entry.name.toLowerCase()) {
414
+ score = Math.max(score, 0.5);
415
+ }
416
+ // Graph neighbor boost (0.2 weight): boost if this skill is neighbor of a hot skill
417
+ let graphBoost = 0;
418
+ for (const hotId of hotSkillIds) {
419
+ const hotEntry = index.find((e) => e.id === hotId);
420
+ if (hotEntry?.neighbors?.includes(entry.id)) {
421
+ graphBoost = 0.2;
422
+ break;
423
+ }
424
+ }
425
+ score += graphBoost * 0.2;
426
+ if (score > 0) {
427
+ results.push({ entry, score });
428
+ }
429
+ }
430
+ return results.sort((a, b) => b.score - a.score);
431
+ }
432
+ // ========================================================================
433
+ // Neighbor Discovery
434
+ // ========================================================================
435
+ function getNeighborIds(entry) {
436
+ return entry.neighbors ?? [];
437
+ }
438
+ // ========================================================================
439
+ // SkillRuntime
440
+ // ========================================================================
441
+ export class SkillRuntime {
442
+ config;
443
+ cwd;
444
+ // Core state
445
+ records = [];
446
+ index = [];
447
+ runtimeStates = new Map();
448
+ turnCount = 0;
449
+ // Telemetry
450
+ events = [];
451
+ // Token budget base (set from compiler.token_budget each turn)
452
+ budgetBase = 100_000;
453
+ // Synonym map (extendable)
454
+ synonyms = { ...DEFAULT_SYNONYMS };
455
+ constructor(config, cwd) {
456
+ this.config = config;
457
+ this.cwd = cwd;
458
+ }
459
+ /** Parse a SKILL.md file. Returns null if invalid or missing required fields. */
460
+ static parseFile(filePath) {
461
+ if (!existsSync(filePath))
462
+ return null;
463
+ const content = readFileSync(filePath, "utf-8");
464
+ return parseSkillMdContent(content, filePath);
465
+ }
466
+ // ---- Initialization ----
467
+ async initialize() {
468
+ if (!this.config.enabled)
469
+ return;
470
+ // 1. Discover skills
471
+ this.records = this.config.searchPaths
472
+ ? discoverSkills(this.cwd, this.config.max_depth, this.config.searchPaths)
473
+ : discoverSkills(this.cwd, this.config.max_depth);
474
+ // 2. Load ARIA-specific metadata
475
+ const meta = loadMergedSkillsMeta(this.cwd);
476
+ // 3. Merge user-provided synonyms from meta (extensible)
477
+ // (no field for custom synonyms yet — future)
478
+ // 4. Build BM25 index
479
+ this.index = buildBM25Index(this.records, meta);
480
+ // 5. Emit discovery events
481
+ for (const entry of this.index) {
482
+ this.emit({
483
+ type: "skill_discovered",
484
+ skill_id: entry.id,
485
+ timestamp: Date.now(),
486
+ });
487
+ // Initialize all skills as cold
488
+ this.runtimeStates.set(entry.id, {
489
+ entry,
490
+ residency: "cold",
491
+ loadedSections: [],
492
+ lastActiveTurn: 0,
493
+ tokenCost: 0,
494
+ body: this.records.find((r) => r.name === entry.id)?.body ?? "",
495
+ directory: this.records.find((r) => r.name === entry.id)?.directory ?? "",
496
+ });
497
+ }
498
+ }
499
+ // ---- Per-turn processing ----
500
+ processUserInput(userInput) {
501
+ if (!this.config.enabled || this.index.length === 0)
502
+ return;
503
+ // 1. Get currently hot skill IDs
504
+ const hotIds = new Set();
505
+ for (const [id, state] of this.runtimeStates) {
506
+ if (state.residency === "hot")
507
+ hotIds.add(id);
508
+ }
509
+ // 2. Rank skills against user input
510
+ const matches = rankSkills(this.index, userInput, hotIds, this.synonyms);
511
+ // 3. Promote top matches to HOT (up to budget)
512
+ // Determine which skills get promoted
513
+ const promoteToHot = [];
514
+ const promoteToWarm = [];
515
+ for (const match of matches) {
516
+ const state = this.runtimeStates.get(match.entry.id);
517
+ if (!state)
518
+ continue;
519
+ if (match.score >= 0.3 && state.residency === "cold") {
520
+ promoteToWarm.push(match.entry.id);
521
+ }
522
+ if (match.score >= 0.5) {
523
+ promoteToHot.push(match.entry.id);
524
+ }
525
+ this.emit({
526
+ type: "skill_matched",
527
+ skill_id: match.entry.id,
528
+ score: Math.round(match.score * 100) / 100,
529
+ residency: state.residency,
530
+ timestamp: Date.now(),
531
+ });
532
+ }
533
+ // 4. Apply neighbor boosting
534
+ for (const hotId of promoteToHot) {
535
+ const hotState = this.runtimeStates.get(hotId);
536
+ if (!hotState)
537
+ continue;
538
+ for (const nid of getNeighborIds(hotState.entry)) {
539
+ const nState = this.runtimeStates.get(nid);
540
+ if (nState && nState.residency === "cold") {
541
+ promoteToWarm.push(nid);
542
+ this.emit({
543
+ type: "skill_neighbor_boosted",
544
+ skill_id: nid,
545
+ residency: "warm",
546
+ timestamp: Date.now(),
547
+ });
548
+ }
549
+ }
550
+ }
551
+ // 5. Apply residency changes
552
+ for (const id of promoteToHot) {
553
+ this.setResidency(id, "hot");
554
+ }
555
+ for (const id of promoteToWarm) {
556
+ if (this.runtimeStates.get(id)?.residency === "cold") {
557
+ this.setResidency(id, "warm");
558
+ }
559
+ }
560
+ // 6. Enforce token budget
561
+ this.enforceBudget();
562
+ }
563
+ endTurn() {
564
+ if (!this.config.enabled)
565
+ return;
566
+ // 1. Update turn count
567
+ this.turnCount++;
568
+ // 2. Demote idle hot → warm
569
+ for (const [id, state] of this.runtimeStates) {
570
+ if (state.residency !== "hot")
571
+ continue;
572
+ const idle = Math.max(0, this.turnCount - state.lastActiveTurn - 1);
573
+ if (idle >= this.config.active_skill_idle_turns) {
574
+ this.demote(id, "warm");
575
+ }
576
+ }
577
+ // 3. Evict idle warm → cold
578
+ for (const [id, state] of this.runtimeStates) {
579
+ if (state.residency !== "warm")
580
+ continue;
581
+ const idle = Math.max(0, this.turnCount - state.lastActiveTurn - 1);
582
+ if (idle >= this.config.warm_skill_eviction_turns) {
583
+ this.evict(id);
584
+ }
585
+ }
586
+ }
587
+ markSkillActive(skillId) {
588
+ const state = this.runtimeStates.get(skillId);
589
+ if (state) {
590
+ state.lastActiveTurn = this.turnCount;
591
+ }
592
+ }
593
+ // ---- Prompt assembly ----
594
+ getSnapshot(tokenBudget) {
595
+ const hot = [];
596
+ const warm = [];
597
+ let tokenUsage = 0;
598
+ for (const state of this.runtimeStates.values()) {
599
+ if (state.residency === "hot") {
600
+ hot.push(state);
601
+ tokenUsage += state.tokenCost;
602
+ }
603
+ else if (state.residency === "warm") {
604
+ warm.push(state);
605
+ }
606
+ }
607
+ return {
608
+ hot: hot.sort((a, b) => a.entry.name.localeCompare(b.entry.name)),
609
+ warm: warm.sort((a, b) => a.entry.name.localeCompare(b.entry.name)),
610
+ tokenUsage,
611
+ tokenBudget,
612
+ };
613
+ }
614
+ /** Build the skills section for the compiled prompt. */
615
+ buildPromptSection(tokenBudget) {
616
+ if (!this.config.enabled)
617
+ return "";
618
+ // Enforce budget before building
619
+ this.setBudgetBase(tokenBudget);
620
+ const snapshot = this.getSnapshot(tokenBudget);
621
+ const lines = ["## Loaded Skills"];
622
+ if (snapshot.hot.length === 0 && snapshot.warm.length === 0) {
623
+ const cold = [...this.runtimeStates.values()]
624
+ .filter((s) => s.residency === "cold")
625
+ .sort((a, b) => a.entry.name.localeCompare(b.entry.name));
626
+ if (cold.length === 0) {
627
+ lines.push("", "(no skills loaded)");
628
+ return lines.join("\n");
629
+ }
630
+ lines.push("", "### Available Skills");
631
+ for (const state of cold) {
632
+ lines.push(`- **${state.entry.name}**: ${state.entry.description}`);
633
+ }
634
+ return lines.join("\n");
635
+ }
636
+ // HOT skills — full bodies of loaded sections
637
+ for (const state of snapshot.hot) {
638
+ lines.push("", `### ${state.entry.name} [HOT]`);
639
+ lines.push(`Tags: ${state.entry.tags.join(", ") || "(none)"}`);
640
+ if (state.entry.trigger)
641
+ lines.push(`Trigger: ${state.entry.trigger}`);
642
+ // Progressive sections
643
+ for (const section of state.loadedSections) {
644
+ const sectionContent = this.getSectionContent(state, section);
645
+ if (sectionContent) {
646
+ lines.push("", sectionContent);
647
+ }
648
+ }
649
+ // If no sections loaded, load full body
650
+ if (state.loadedSections.length === 0 && state.body) {
651
+ lines.push("", state.body);
652
+ }
653
+ }
654
+ // WARM skills — one-line stubs
655
+ if (snapshot.warm.length > 0) {
656
+ lines.push("", "### Standing By");
657
+ for (const state of snapshot.warm) {
658
+ const tags = state.entry.tags.length > 0 ? ` [${state.entry.tags.slice(0, 3).join(", ")}]` : "";
659
+ lines.push(`- ${state.entry.name}${tags}`);
660
+ }
661
+ }
662
+ return lines.join("\n");
663
+ }
664
+ getSectionContent(state, section) {
665
+ if (!state.entry.sectionRanges)
666
+ return "";
667
+ const range = state.entry.sectionRanges[section];
668
+ if (!range)
669
+ return "";
670
+ return extractSection(state.body, range);
671
+ }
672
+ // ---- Residency management ----
673
+ setResidency(id, target) {
674
+ const state = this.runtimeStates.get(id);
675
+ if (!state || state.residency === target)
676
+ return;
677
+ const prev = state.residency;
678
+ state.residency = target;
679
+ state.lastActiveTurn = this.turnCount;
680
+ if (target === "hot") {
681
+ // Progressive hydration: load planner first, execution on active use
682
+ this.hydrateSkill(id);
683
+ }
684
+ if (target === "hot") {
685
+ this.emit({
686
+ type: "skill_loaded",
687
+ skill_id: id,
688
+ residency: target,
689
+ prev_residency: prev,
690
+ sections: state.loadedSections,
691
+ token_cost: state.tokenCost,
692
+ timestamp: Date.now(),
693
+ });
694
+ }
695
+ else {
696
+ this.emit({
697
+ type: "skill_promoted",
698
+ skill_id: id,
699
+ residency: target,
700
+ prev_residency: prev,
701
+ sections: state.loadedSections,
702
+ token_cost: state.tokenCost,
703
+ timestamp: Date.now(),
704
+ });
705
+ }
706
+ }
707
+ demote(id, target) {
708
+ const state = this.runtimeStates.get(id);
709
+ if (!state)
710
+ return;
711
+ const prev = state.residency;
712
+ state.residency = target;
713
+ state.loadedSections = [];
714
+ state.tokenCost = 0;
715
+ this.emit({
716
+ type: "skill_demoted",
717
+ skill_id: id,
718
+ residency: target,
719
+ prev_residency: prev,
720
+ timestamp: Date.now(),
721
+ });
722
+ }
723
+ evict(id) {
724
+ const state = this.runtimeStates.get(id);
725
+ if (!state)
726
+ return;
727
+ state.residency = "cold";
728
+ state.loadedSections = [];
729
+ state.tokenCost = 0;
730
+ this.emit({
731
+ type: "skill_evicted",
732
+ skill_id: id,
733
+ residency: "cold",
734
+ timestamp: Date.now(),
735
+ });
736
+ }
737
+ // ---- Progressive Hydration ----
738
+ hydrateSkill(id) {
739
+ const state = this.runtimeStates.get(id);
740
+ if (!state)
741
+ return;
742
+ // Load planner when present; otherwise leave sections empty so the prompt
743
+ // builder falls back to the full skill body.
744
+ if (state.entry.sectionRanges?.planner && !state.loadedSections.includes("planner")) {
745
+ state.loadedSections.push("planner");
746
+ this.emit({
747
+ type: "skill_hydrated",
748
+ skill_id: id,
749
+ sections: ["planner"],
750
+ timestamp: Date.now(),
751
+ });
752
+ }
753
+ // Recompute token cost
754
+ state.tokenCost = this.computeTokenCost(state);
755
+ }
756
+ /** Load execution section (called when tool execution starts) */
757
+ hydrateExecution(id) {
758
+ const state = this.runtimeStates.get(id);
759
+ if (!state || state.residency !== "hot")
760
+ return;
761
+ if (!state.loadedSections.includes("execution")) {
762
+ state.loadedSections.push("execution");
763
+ state.tokenCost = this.computeTokenCost(state);
764
+ this.emit({
765
+ type: "skill_hydrated",
766
+ skill_id: id,
767
+ sections: ["execution"],
768
+ timestamp: Date.now(),
769
+ });
770
+ }
771
+ }
772
+ /** HOT skill IDs for the current turn. */
773
+ getHotSkillIds() {
774
+ return [...this.runtimeStates.entries()]
775
+ .filter(([, state]) => state.residency === "hot")
776
+ .map(([id]) => id);
777
+ }
778
+ /** Load execution sections for all HOT skills (called when tool execution starts). */
779
+ hydrateExecutionForHotSkills() {
780
+ for (const id of this.getHotSkillIds()) {
781
+ this.hydrateExecution(id);
782
+ this.markSkillActive(id);
783
+ }
784
+ }
785
+ /** Load recovery sections for all HOT skills (called on tool failure). */
786
+ hydrateRecoveryForHotSkills() {
787
+ for (const id of this.getHotSkillIds()) {
788
+ this.hydrateRecovery(id);
789
+ this.markSkillActive(id);
790
+ }
791
+ }
792
+ /** Load recovery section (called on failure) */
793
+ hydrateRecovery(id) {
794
+ const state = this.runtimeStates.get(id);
795
+ if (!state || state.residency !== "hot")
796
+ return;
797
+ if (!state.loadedSections.includes("recovery")) {
798
+ state.loadedSections.push("recovery");
799
+ state.tokenCost = this.computeTokenCost(state);
800
+ this.emit({
801
+ type: "skill_hydrated",
802
+ skill_id: id,
803
+ sections: ["recovery"],
804
+ timestamp: Date.now(),
805
+ });
806
+ }
807
+ }
808
+ // ---- Budget ----
809
+ computeTokenCost(state) {
810
+ let total = 0;
811
+ for (const section of state.loadedSections) {
812
+ const content = this.getSectionContent(state, section);
813
+ total += Math.ceil(content.length / 4);
814
+ }
815
+ if (state.loadedSections.length === 0 && state.body) {
816
+ total = Math.ceil(state.body.length / 4);
817
+ }
818
+ return Math.min(total, state.entry.maxTokens);
819
+ }
820
+ getSkillsTokenBudget() {
821
+ return Math.floor(this.budgetBase * this.config.max_token_budget_ratio);
822
+ }
823
+ enforceBudget() {
824
+ const budget = this.getSkillsTokenBudget();
825
+ const hotStates = [...this.runtimeStates.values()]
826
+ .filter((s) => s.residency === "hot")
827
+ .sort((a, b) => a.lastActiveTurn - b.lastActiveTurn);
828
+ let totalCost = hotStates.reduce((sum, s) => sum + s.tokenCost, 0);
829
+ while (totalCost > budget && hotStates.length > 0) {
830
+ const victim = hotStates.shift();
831
+ this.emit({
832
+ type: "skill_budget_exceeded",
833
+ skill_id: victim.entry.id,
834
+ token_cost: victim.tokenCost,
835
+ timestamp: Date.now(),
836
+ });
837
+ this.demote(victim.entry.id, "warm");
838
+ totalCost -= victim.tokenCost;
839
+ }
840
+ }
841
+ /** Update the budget base (called each turn with compiler.token_budget). */
842
+ setBudgetBase(totalTokenBudget) {
843
+ this.budgetBase = totalTokenBudget;
844
+ this.enforceBudget();
845
+ }
846
+ // ---- Telemetry ----
847
+ emit(event) {
848
+ this.events.push(event);
849
+ }
850
+ drainEvents() {
851
+ const drained = this.events;
852
+ this.events = [];
853
+ return drained;
854
+ }
855
+ getEvents() {
856
+ return [...this.events];
857
+ }
858
+ // ---- Queries ----
859
+ getIndex() {
860
+ return [...this.index];
861
+ }
862
+ getRuntimeStates() {
863
+ return new Map(this.runtimeStates);
864
+ }
865
+ getSkillCount() {
866
+ return this.index.length;
867
+ }
868
+ getResidencyCounts() {
869
+ let hot = 0;
870
+ let warm = 0;
871
+ let cold = 0;
872
+ for (const state of this.runtimeStates.values()) {
873
+ if (state.residency === "hot")
874
+ hot++;
875
+ else if (state.residency === "warm")
876
+ warm++;
877
+ else
878
+ cold++;
879
+ }
880
+ return { hot, warm, cold };
881
+ }
882
+ isEnabled() {
883
+ return this.config.enabled;
884
+ }
885
+ }