aiden-runtime 3.18.0 → 3.19.4

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.
@@ -3,39 +3,19 @@
3
3
  // DevOS — Autonomous AI Execution System
4
4
  // Copyright (c) 2026 Shiva Deore. All rights reserved.
5
5
  // ============================================================
6
- var __importDefault = (this && this.__importDefault) || function (mod) {
7
- return (mod && mod.__esModule) ? mod : { "default": mod };
8
- };
9
6
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.AIDEN_RESPONDER_SYSTEM = exports.AIDEN_STREAM_SYSTEM = exports.AIDEN_IDENTITY = exports.AIDEN_REAL_TOOLS = exports.SOUL = void 0;
7
+ exports.AIDEN_RESPONDER_SYSTEM = exports.AIDEN_IDENTITY = exports.AIDEN_REAL_TOOLS = void 0;
8
+ exports.getLiveSoul = getLiveSoul;
11
9
  // core/aidenPersonality.ts — Unified Aiden identity + system prompts
12
10
  //
13
11
  // Single source of truth for personality, tone, and capability declarations.
14
12
  // All system prompts across agentLoop.ts and server.ts reference this.
15
- const fs_1 = __importDefault(require("fs"));
16
- const path_1 = __importDefault(require("path"));
17
- // ── Workspace root same logic as api/server.ts ──────────────
18
- // In Electron: AIDEN_USER_DATA = %APPDATA%/devos-ai
19
- // In direct dev (npm run dev): process.cwd() = project root
20
- const SOUL_WORKSPACE_ROOT = process.env.AIDEN_USER_DATA || process.cwd();
21
- // ── Load SOUL.md at startup ────────────────────────────────────
22
- function loadSoul() {
23
- try {
24
- // Primary: workspace/SOUL.md (installed Electron app location)
25
- const wsPath = path_1.default.join(SOUL_WORKSPACE_ROOT, 'workspace', 'SOUL.md');
26
- if (fs_1.default.existsSync(wsPath)) {
27
- return fs_1.default.readFileSync(wsPath, 'utf-8');
28
- }
29
- // Fallback: SOUL.md at root (direct npm run dev, cwd = project root)
30
- const rootPath = path_1.default.join(process.cwd(), 'SOUL.md');
31
- if (fs_1.default.existsSync(rootPath)) {
32
- return fs_1.default.readFileSync(rootPath, 'utf-8');
33
- }
34
- }
35
- catch { }
36
- return '';
13
+ const protectedContext_1 = require("./protectedContext");
14
+ // ── Live SOUL.md accessor — reads from hash-cached ProtectedContextManager.
15
+ // Never frozen: always reflects the current on-disk content.
16
+ function getLiveSoul() {
17
+ return protectedContext_1.protectedContextManager.getProtectedContext().soul;
37
18
  }
38
- exports.SOUL = loadSoul();
39
19
  // ── Tool list ─────────────────────────────────────────────────
40
20
  // Keep in sync with TOOLS in toolRegistry.ts
41
21
  exports.AIDEN_REAL_TOOLS = `
@@ -142,16 +122,10 @@ Good: "I have 48 built-in tools: web_search, file_write, run_python... [lists re
142
122
  - You're not the user's voice — be careful in
143
123
  group chats and external communications.
144
124
  `.trim();
145
- // ── Stream chat system prompt (no tools available) ────────────
146
- exports.AIDEN_STREAM_SYSTEM = `${exports.SOUL ? exports.SOUL + '\n\n' : ''}${exports.AIDEN_IDENTITY}
147
-
148
- You are in direct chat mode — no tools are running right now.
149
- Answer from your knowledge. Be concise and direct.
150
- If the question needs real-time data (weather, stocks, news) — tell the user to
151
- rephrase as a task (e.g. "search for..." or "get me the latest...") and you will
152
- execute the right tool automatically.`;
153
125
  // ── Responder system prompt (post-execution) ──────────────────
154
- const AIDEN_RESPONDER_SYSTEM = (userName, date) => `${exports.SOUL ? exports.SOUL + '\n\n' : ''}${exports.AIDEN_IDENTITY}
126
+ const AIDEN_RESPONDER_SYSTEM = (userName, date) => {
127
+ const soul = getLiveSoul();
128
+ return `${soul ? soul + '\n\n' : ''}${exports.AIDEN_IDENTITY}
155
129
 
156
130
  You just executed real tools and have their actual output.
157
131
  Current date: ${date}
@@ -163,4 +137,5 @@ REPORT RESULTS:
163
137
  - If multiple steps ran: summarize the outcome, not each individual step
164
138
  - If a step failed: acknowledge it clearly and explain what worked
165
139
  - For research tasks: analyze and synthesize — don't just re-paste the raw data`;
140
+ };
166
141
  exports.AIDEN_RESPONDER_SYSTEM = AIDEN_RESPONDER_SYSTEM;
@@ -23,6 +23,7 @@ const AUX_ENDPOINTS = {
23
23
  nvidia: 'https://integrate.api.nvidia.com/v1/chat/completions',
24
24
  github: 'https://models.inference.ai.azure.com/chat/completions',
25
25
  boa: 'https://api.boa.ai/v1/chat/completions',
26
+ mistral: 'https://api.mistral.ai/v1/chat/completions',
26
27
  };
27
28
  const DEFAULT_AUX_CONFIG = {
28
29
  preferLocal: true,
@@ -11,6 +11,7 @@ exports.moveMouse = moveMouse;
11
11
  exports.clickMouse = clickMouse;
12
12
  exports.typeText = typeText;
13
13
  exports.pressKey = pressKey;
14
+ exports.resolveScreenshotPath = resolveScreenshotPath;
14
15
  exports.takeScreenshot = takeScreenshot;
15
16
  exports.readScreen = readScreen;
16
17
  exports.openBrowser = openBrowser;
@@ -161,10 +162,25 @@ $wsh.SendKeys('${mapped}')
161
162
  return `Pressed: ${key}`;
162
163
  }
163
164
  // ── SCREENSHOT ─────────────────────────────────────────────────
164
- async function takeScreenshot() {
165
- const filename = `screenshot_${Date.now()}.png`;
166
- const filepath = path_1.default.join(SCREENSHOTS_DIR, filename);
167
- const escaped = filepath.replace(/\\/g, '\\\\');
165
+ /**
166
+ * Pure path resolver — exported for unit tests.
167
+ * Returns the absolute path where the screenshot should be saved.
168
+ * Throws if outputPath is provided but is not absolute.
169
+ */
170
+ function resolveScreenshotPath(outputPath) {
171
+ if (outputPath !== undefined && outputPath !== '') {
172
+ const isAbsolute = /^[A-Za-z]:[/\\]/.test(outputPath) || outputPath.startsWith('/');
173
+ if (!isAbsolute)
174
+ throw new Error(`outputPath must be an absolute path, got: ${outputPath}`);
175
+ return outputPath;
176
+ }
177
+ return path_1.default.join(SCREENSHOTS_DIR, `screenshot_${Date.now()}.png`);
178
+ }
179
+ async function takeScreenshot(opts) {
180
+ const filepath = resolveScreenshotPath(opts?.outputPath);
181
+ const useDefault = !opts?.outputPath;
182
+ // C3b: No backslash escaping — PS single-quoted strings are literal, \\ would be passed verbatim to .NET
183
+ const escaped = filepath;
168
184
  await psFile(`
169
185
  Add-Type -AssemblyName System.Windows.Forms
170
186
  Add-Type -AssemblyName System.Drawing
@@ -176,21 +192,23 @@ $bitmap.Save('${escaped}')
176
192
  $graphics.Dispose()
177
193
  $bitmap.Dispose()
178
194
  `);
179
- // Trim old screenshots — keep only last 10
180
- try {
181
- const files = fs_1.default.readdirSync(SCREENSHOTS_DIR)
182
- .filter(f => f.endsWith('.png'))
183
- .sort();
184
- if (files.length > 10) {
185
- files.slice(0, files.length - 10).forEach(f => {
186
- try {
187
- fs_1.default.unlinkSync(path_1.default.join(SCREENSHOTS_DIR, f));
188
- }
189
- catch { }
190
- });
195
+ // Trim old screenshots — keep only last 10 (default dir only)
196
+ if (useDefault) {
197
+ try {
198
+ const files = fs_1.default.readdirSync(SCREENSHOTS_DIR)
199
+ .filter(f => f.endsWith('.png'))
200
+ .sort();
201
+ if (files.length > 10) {
202
+ files.slice(0, files.length - 10).forEach(f => {
203
+ try {
204
+ fs_1.default.unlinkSync(path_1.default.join(SCREENSHOTS_DIR, f));
205
+ }
206
+ catch { }
207
+ });
208
+ }
191
209
  }
210
+ catch { }
192
211
  }
193
- catch { }
194
212
  if (fs_1.default.existsSync(filepath))
195
213
  return filepath;
196
214
  throw new Error('Screenshot failed — file not created');
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildProtectedContextBlock = buildProtectedContextBlock;
4
+ function buildProtectedContextBlock(ctx, previousHash, sessionId) {
5
+ const sections = [];
6
+ const soulUnchanged = previousHash !== undefined && ctx.hash === previousHash;
7
+ if (soulUnchanged) {
8
+ sections.push(`Identity (SOUL.md): [unchanged from previous turn, hash: ${ctx.hash}]`);
9
+ }
10
+ else if (ctx.soul) {
11
+ sections.push(`Identity (SOUL.md):\n${ctx.soul}`);
12
+ }
13
+ if (ctx.user)
14
+ sections.push(`User (USER.md):\n${ctx.user}`);
15
+ if (ctx.goals)
16
+ sections.push(`Active Goals (GOALS.md):\n${ctx.goals}`);
17
+ if (ctx.standingOrders)
18
+ sections.push(`Standing Orders (STANDING_ORDERS.md):\n${ctx.standingOrders}`);
19
+ if (ctx.lessons)
20
+ sections.push(`Lessons Learned (LESSONS.md):\n${ctx.lessons}`);
21
+ if (sections.length === 0)
22
+ return '';
23
+ const block = [
24
+ '[PROTECTED CONTEXT — AUTHORITATIVE, REFRESHED THIS TURN]',
25
+ '',
26
+ sections.join('\n\n'),
27
+ '',
28
+ '[END PROTECTED CONTEXT]',
29
+ ].join('\n');
30
+ // C4: structured per-turn protected-context metrics (always-on, stderr only)
31
+ const sidShort = sessionId ? sessionId.slice(0, 8) : 'nosess ';
32
+ const soulMode = soulUnchanged ? 'REF' : (ctx.soul ? 'FULL' : 'EMPTY');
33
+ const tokens = Math.round(block.length / 4);
34
+ const hashShort = ctx.hash.slice(0, 8);
35
+ const files = ctx.changedFiles.length > 0 ? ctx.changedFiles.join(',') : 'none';
36
+ process.stderr.write(`[ProtectedCtx] sessionId=${sidShort} cacheHit=${soulUnchanged}` +
37
+ ` soul=${soulMode} tokens=${tokens} hash=${hashShort} files=${files}\n`);
38
+ return block;
39
+ }
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // DevOS — Autonomous AI Execution System
4
+ // Copyright (c) 2026 Shiva Deore. All rights reserved.
5
+ // ============================================================
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.buildDiagnostic = buildDiagnostic;
8
+ function buildDiagnostic(info) {
9
+ const lines = [
10
+ `Couldn't ${info.tool}: ${info.error}`,
11
+ `Provider: ${info.provider ?? 'unknown'}, retries: ${info.retries}`,
12
+ ];
13
+ if (info.fallbackTried) {
14
+ lines.push(`Fell back to ${info.fallbackTried.name} → ${info.fallbackTried.outcome}`);
15
+ }
16
+ if (info.suggestion) {
17
+ lines.push(`Suggestion: ${info.suggestion}`);
18
+ }
19
+ return lines.join('\n');
20
+ }
@@ -6,6 +6,7 @@
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.matchFastPath = matchFastPath;
8
8
  // core/fastPathExpansion.ts — Phase 5 of Prompt 10.
9
+ // v3.19 C3: action intents are never fast-pathed (they need the planner).
9
10
  //
10
11
  // Classifies messages that can be answered directly by streamChat without
11
12
  // going through the planner, cutting 2–8s of planner latency for ~60% of
@@ -17,6 +18,7 @@ exports.matchFastPath = matchFastPath;
17
18
  // - Asks for short creative output (joke, quote, haiku)
18
19
  // - Does NOT contain signals for file I/O, code execution, web search,
19
20
  // git, screen control, or multi-step planning keywords.
21
+ const actionVerbDetector_1 = require("./actionVerbDetector");
20
22
  // ── Patterns that indicate a tool-less, plannable-free response ──────────────
21
23
  /** Positive patterns: message can likely be answered without any tools. */
22
24
  const FAST_PATH_PATTERNS = [
@@ -54,6 +56,8 @@ const PLANNER_REQUIRED_PATTERNS = [
54
56
  /\b(then|after that|next|first .+ then|step by step|automate|workflow|pipeline|schedule|every (day|hour|minute)|on a schedule)\b/i,
55
57
  // Email / calendar
56
58
  /\b(send (email|mail|message)|check (email|calendar)|schedule (meeting|call)|remind me)\b/i,
59
+ // Music / media playback state — needs now_playing tool
60
+ /\b(now playing|currently playing|what (music|song|track|artist|album)|am i playing|is (playing|on))\b/i,
57
61
  ];
58
62
  /**
59
63
  * Returns true if this message can skip the planner and go directly to streamChat.
@@ -61,6 +65,9 @@ const PLANNER_REQUIRED_PATTERNS = [
61
65
  */
62
66
  function matchFastPath(message) {
63
67
  const trimmed = message.trim();
68
+ // Action intents always need the planner — never fast-path them
69
+ if ((0, actionVerbDetector_1.isActionIntent)(trimmed))
70
+ return false;
64
71
  // Very short messages are usually conversational
65
72
  if (trimmed.length < 20 && !/\b(run|exec|create|write|search|find|git)\b/i.test(trimmed)) {
66
73
  return true;
@@ -41,6 +41,7 @@ exports.nextId = nextId;
41
41
  exports.appendRecord = appendRecord;
42
42
  exports.loadAllRecords = loadAllRecords;
43
43
  exports.loadRecordById = loadRecordById;
44
+ exports.removeRecords = removeRecords;
44
45
  exports.assignId = assignId;
45
46
  exports.runMigrationIfNeeded = runMigrationIfNeeded;
46
47
  /**
@@ -109,6 +110,21 @@ function loadRecordById(id) {
109
110
  const all = loadAllRecords();
110
111
  return all.find(r => r.id === id) ?? null;
111
112
  }
113
+ /**
114
+ * C11: Remove records matching a predicate from records.jsonl.
115
+ * Rewrites the file with only the non-matching records.
116
+ * Returns the count of removed entries.
117
+ */
118
+ function removeRecords(predicate) {
119
+ const all = loadAllRecords();
120
+ const kept = all.filter(r => !predicate(r));
121
+ const removed = all.length - kept.length;
122
+ if (removed > 0) {
123
+ _ensureDir();
124
+ fs.writeFileSync(RECORDS_FILE, kept.map(r => JSON.stringify(r)).join('\n') + (kept.length ? '\n' : ''), 'utf-8');
125
+ }
126
+ return removed;
127
+ }
112
128
  // ── ID assignment helper ────────────────────────────────────────────────────
113
129
  function assignId(partial) {
114
130
  const record = {
@@ -57,7 +57,7 @@ exports.pluginHooks = {
57
57
  };
58
58
  exports.loadedFlatPlugins = [];
59
59
  // ── Plugin context (passed to init / onLoad) ──────────────────
60
- function makeContext(pluginName) {
60
+ function makeContext(pluginName, services = {}) {
61
61
  return {
62
62
  registerTool(def) {
63
63
  (0, toolRegistry_1.registerExternalTool)(def.name, def.execute, pluginName);
@@ -76,10 +76,13 @@ function makeContext(pluginName) {
76
76
  log(...args) {
77
77
  console.log(`[Plugin:${pluginName}]`, ...args);
78
78
  },
79
+ // Slash-command registry — register/unregister commands that appear in the
80
+ // Tab-dropdown. See PluginServices JSDoc above for full usage.
81
+ commandCatalog: services.commandCatalog ?? null,
79
82
  };
80
83
  }
81
84
  // ── Loader ────────────────────────────────────────────────────
82
- async function loadPlugins(pluginDir) {
85
+ async function loadPlugins(pluginDir, services = {}) {
83
86
  if (!fs.existsSync(pluginDir)) {
84
87
  fs.mkdirSync(pluginDir, { recursive: true });
85
88
  return;
@@ -106,7 +109,7 @@ async function loadPlugins(pluginDir) {
106
109
  console.warn(`[PluginLoader] ${file}: no init() — skipping`);
107
110
  continue;
108
111
  }
109
- const ctx = makeContext(name);
112
+ const ctx = makeContext(name, services);
110
113
  const dispose = await initFn.call(def.default ?? def, ctx);
111
114
  exports.loadedFlatPlugins.push({
112
115
  name,
@@ -127,7 +130,7 @@ async function loadPlugins(pluginDir) {
127
130
  }
128
131
  }
129
132
  // ── Reload — dispose old plugins then reload all flat plugins ─
130
- async function reloadPlugins(pluginDir) {
133
+ async function reloadPlugins(pluginDir, services = {}) {
131
134
  // Dispose in reverse order
132
135
  for (let i = exports.loadedFlatPlugins.length - 1; i >= 0; i--) {
133
136
  const p = exports.loadedFlatPlugins[i];
@@ -145,7 +148,7 @@ async function reloadPlugins(pluginDir) {
145
148
  exports.pluginHooks.postTool.length = 0;
146
149
  exports.pluginHooks.onSessionStart.length = 0;
147
150
  exports.pluginHooks.onSessionEnd.length = 0;
148
- await loadPlugins(pluginDir);
151
+ await loadPlugins(pluginDir, services);
149
152
  }
150
153
  // ── Status ────────────────────────────────────────────────────
151
154
  function listFlatPlugins() {
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.protectedContextManager = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const crypto_1 = __importDefault(require("crypto"));
10
+ const WORKSPACE_ROOT = process.env.AIDEN_USER_DATA || process.cwd();
11
+ const WORKSPACE_DIR = path_1.default.join(WORKSPACE_ROOT, 'workspace');
12
+ // SOUL.md has two possible locations; try workspace first, fall back to root.
13
+ const SOUL_PATHS = [
14
+ path_1.default.join(WORKSPACE_DIR, 'SOUL.md'),
15
+ path_1.default.join(process.cwd(), 'SOUL.md'),
16
+ ];
17
+ const PROTECTED_FILES = {
18
+ soul: SOUL_PATHS, // array = try in order
19
+ user: [path_1.default.join(WORKSPACE_DIR, 'USER.md')],
20
+ goals: [path_1.default.join(WORKSPACE_DIR, 'GOALS.md')],
21
+ standingOrders: [path_1.default.join(WORKSPACE_DIR, 'STANDING_ORDERS.md')],
22
+ lessons: [path_1.default.join(WORKSPACE_DIR, 'LESSONS.md')],
23
+ };
24
+ function sha1(s) {
25
+ return crypto_1.default.createHash('sha1').update(s).digest('hex');
26
+ }
27
+ function readFirst(candidates) {
28
+ for (const p of candidates) {
29
+ try {
30
+ if (fs_1.default.existsSync(p))
31
+ return fs_1.default.readFileSync(p, 'utf-8');
32
+ }
33
+ catch { }
34
+ }
35
+ return '';
36
+ }
37
+ function diskHash(candidates) {
38
+ return sha1(readFirst(candidates));
39
+ }
40
+ class ProtectedContextManager {
41
+ constructor() {
42
+ this.cache = {
43
+ soul: { content: '', hash: '' },
44
+ user: { content: '', hash: '' },
45
+ goals: { content: '', hash: '' },
46
+ standingOrders: { content: '', hash: '' },
47
+ lessons: { content: '', hash: '' },
48
+ };
49
+ this.compositeHash = '';
50
+ this._totalReads = 0;
51
+ this._cacheHits = 0;
52
+ this._lastRefreshMs = 0;
53
+ this.refresh();
54
+ }
55
+ // Force re-read all 5 files from disk (called at boot).
56
+ refresh() {
57
+ for (const key of Object.keys(PROTECTED_FILES)) {
58
+ const candidates = PROTECTED_FILES[key];
59
+ const content = readFirst(candidates);
60
+ this.cache[key] = { content, hash: sha1(content) };
61
+ }
62
+ this.compositeHash = this._buildComposite();
63
+ this._lastRefreshMs = Date.now();
64
+ }
65
+ // Returns cached context, re-reading only files whose on-disk hash changed.
66
+ getProtectedContext() {
67
+ this._totalReads++;
68
+ const changedFiles = [];
69
+ for (const key of Object.keys(PROTECTED_FILES)) {
70
+ if (this.isStale(key)) {
71
+ const content = readFirst(PROTECTED_FILES[key]);
72
+ this.cache[key] = { content, hash: sha1(content) };
73
+ changedFiles.push(key);
74
+ }
75
+ }
76
+ if (changedFiles.length > 0) {
77
+ this.compositeHash = this._buildComposite();
78
+ this._lastRefreshMs = Date.now();
79
+ }
80
+ else {
81
+ this._cacheHits++;
82
+ }
83
+ return {
84
+ soul: this.cache.soul.content,
85
+ user: this.cache.user.content,
86
+ goals: this.cache.goals.content,
87
+ standingOrders: this.cache.standingOrders.content,
88
+ lessons: this.cache.lessons.content,
89
+ hash: this.compositeHash,
90
+ changedFiles,
91
+ };
92
+ }
93
+ getMetrics() {
94
+ return {
95
+ totalReads: this._totalReads,
96
+ cacheHits: this._cacheHits,
97
+ lastRefreshMs: this._lastRefreshMs,
98
+ currentHash: this.compositeHash,
99
+ };
100
+ }
101
+ // True if the on-disk hash for a file differs from our cached hash.
102
+ isStale(key) {
103
+ return diskHash(PROTECTED_FILES[key]) !== this.cache[key].hash;
104
+ }
105
+ _buildComposite() {
106
+ const parts = Object.keys(PROTECTED_FILES)
107
+ .map(k => this.cache[k].hash)
108
+ .join(':');
109
+ return sha1(parts).slice(0, 16);
110
+ }
111
+ }
112
+ exports.protectedContextManager = new ProtectedContextManager();
@@ -41,6 +41,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
41
41
  };
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
43
  exports.skillTeacher = exports.SkillTeacher = void 0;
44
+ exports.validateSkillName = validateSkillName;
45
+ exports.validateSkillTask = validateSkillTask;
44
46
  // core/skillTeacher.ts — Self-learning skill generation.
45
47
  // After every successful plan execution, records the tool sequence,
46
48
  // generates a SKILL.md using the LLM, and promotes to "approved"
@@ -85,6 +87,45 @@ function extractSkillName(task, tools) {
85
87
  .slice(0, 3);
86
88
  return words.join('_') || 'general_task';
87
89
  }
90
+ // ── C12: Skill pollution prevention — exported for testing ──────
91
+ const QUESTION_WORD_RE = /^(what|where|why|when|who|how|can|could|would|should|is|are)_/;
92
+ const PRONOUN_RE = /^(its|im|youre|whats|theyre|were)_/;
93
+ const PERSONAL_ID_RE = /(^|_)(users|shiva|admin|desktop|appdata)(_|$)/;
94
+ /**
95
+ * Validate a candidate skill name. Returns null if valid,
96
+ * or a rejection reason string.
97
+ */
98
+ function validateSkillName(name) {
99
+ const words = name.split('_');
100
+ if (words.length > 4)
101
+ return `name has >4 underscore-separated words (${words.length})`;
102
+ if (QUESTION_WORD_RE.test(name))
103
+ return 'name starts with question word';
104
+ if (PRONOUN_RE.test(name))
105
+ return 'name starts with pronoun pattern';
106
+ if (PERSONAL_ID_RE.test(name))
107
+ return 'name contains personal identifier';
108
+ return null;
109
+ }
110
+ /**
111
+ * Validate a candidate skill task description. Returns null if valid,
112
+ * or a rejection reason string.
113
+ * @param task - The skill's task description
114
+ * @param userMessage - Original user message (for verbatim check)
115
+ */
116
+ function validateSkillTask(task, userMessage) {
117
+ const norm = task.toLowerCase().trim();
118
+ if (norm.length < 30)
119
+ return `task too short (${norm.length} chars, min 30)`;
120
+ if (norm.endsWith('?'))
121
+ return 'task is a question';
122
+ if (userMessage) {
123
+ const msgNorm = userMessage.toLowerCase().trim();
124
+ if (norm === msgNorm)
125
+ return 'task is verbatim copy of user message';
126
+ }
127
+ return null;
128
+ }
88
129
  // ── SKILL.md generator ─────────────────────────────────────────
89
130
  async function generateSkillContent(skillName, task, tools, duration, llmCaller, apiKey, model, provider) {
90
131
  const prompt = `Generate a SKILL.md file for DevOS based on this successful task execution.
@@ -213,6 +254,28 @@ class SkillTeacher {
213
254
  console.log(`[SkillTeacher] Rejected low-quality skill: "${skillName}"`);
214
255
  return;
215
256
  }
257
+ // ── C7: Destructive-skill prevention ──────────────────────
258
+ // Reject any skill that pairs shell_exec with a destructive task description.
259
+ // Prevents poisoned skills like "delete_users_shiva" that accidentally learned
260
+ // from test-triggered or misrouted Delete/Remove operations.
261
+ const DESTRUCTIVE_TASK_RE = /\b(delete|remove|rm\s|del\s|wipe|purge|erase|format|uninstall|drop\s+table|truncate)\b/i;
262
+ const usesShellExec = tools.some(t => t === 'shell_exec');
263
+ if (DESTRUCTIVE_TASK_RE.test(task) && usesShellExec) {
264
+ process.stderr.write(`[SkillTeacher] Rejected destructive skill: "${skillName}" (task="${task.slice(0, 60)}")\n`);
265
+ return;
266
+ }
267
+ // ── C12: Name pollution prevention ──────────────────────────
268
+ const nameRejection = validateSkillName(skillName);
269
+ if (nameRejection) {
270
+ process.stderr.write(`[SkillTeacher] Rejected "${skillName}": ${nameRejection}\n`);
271
+ return;
272
+ }
273
+ // ── C12: Task content validation ────────────────────────────
274
+ const taskRejection = validateSkillTask(task);
275
+ if (taskRejection) {
276
+ process.stderr.write(`[SkillTeacher] Rejected "${skillName}": ${taskRejection}\n`);
277
+ return;
278
+ }
216
279
  // ── Session rate limit — max SESSION_SKILL_LIMIT new skills ─
217
280
  if (_sessionSkillsCreated >= SESSION_SKILL_LIMIT) {
218
281
  console.log(`[SkillTeacher] Session limit reached (${SESSION_SKILL_LIMIT}), skipping: "${skillName}"`);
@@ -53,6 +53,7 @@ const fs_1 = __importDefault(require("fs"));
53
53
  const path_1 = __importDefault(require("path"));
54
54
  const os_1 = __importDefault(require("os"));
55
55
  const toolRegistry_1 = require("./toolRegistry");
56
+ const memoryIds_1 = require("./memoryIds");
56
57
  const conversationMemory_1 = require("./conversationMemory");
57
58
  const learningMemory_1 = require("./learningMemory");
58
59
  const skillLoader_1 = require("./skillLoader");
@@ -188,6 +189,40 @@ async function toolChannelsStatus(_) {
188
189
  return { success: true, output: 'Provider status unavailable.' };
189
190
  }
190
191
  }
192
+ // ── Memory store ──────────────────────────────────────────────────────────────
193
+ async function toolMemoryStore(input) {
194
+ // Accept any reasonable field name the planner might guess
195
+ const fact = String(input?.fact || input?.content || input?.text ||
196
+ input?.preference || input?.value || input?.memory ||
197
+ input?.note || input?.data || input?.information || input?.detail ||
198
+ input?.message || input?.entry || input?.record ||
199
+ (input && typeof input === 'object' ? Object.values(input).find(v => typeof v === 'string' && v.trim().length > 0) : '') || '').trim();
200
+ if (!fact)
201
+ return { success: false, output: 'No fact provided. Pass { fact: "the thing to remember" }' };
202
+ const record = (0, memoryIds_1.assignId)({
203
+ timestamp: new Date().toISOString(),
204
+ type: input?.type ?? 'fact',
205
+ content: fact,
206
+ summary: fact.slice(0, 100),
207
+ tags: Array.isArray(input?.tags) ? input.tags : [],
208
+ });
209
+ return { success: true, output: `Stored as ${record.id}: ${record.summary}` };
210
+ }
211
+ // ── Memory forget (C11) ──────────────────────────────────────────────────────
212
+ async function toolMemoryForget(input) {
213
+ const keyword = String(input?.fact || input?.keyword || input?.content || input?.text ||
214
+ input?.query || input?.topic || input?.subject ||
215
+ (input && typeof input === 'object'
216
+ ? Object.values(input).find(v => typeof v === 'string' && v.trim().length > 0)
217
+ : '') || '').trim().toLowerCase();
218
+ if (!keyword)
219
+ return { success: false, output: 'No keyword provided. Pass { fact: "thing to forget" }' };
220
+ const removed = (0, memoryIds_1.removeRecords)(r => r.content.toLowerCase().includes(keyword) ||
221
+ r.summary.toLowerCase().includes(keyword));
222
+ if (removed === 0)
223
+ return { success: true, output: `No memory entries matched "${keyword}".` };
224
+ return { success: true, output: `Removed ${removed} memory entry(s) matching "${keyword}".` };
225
+ }
191
226
  // ── Goals tool ────────────────────────────────────────────────────────────────
192
227
  async function toolGoals(_) {
193
228
  const summary = (0, goalTracker_1.getActiveGoalsSummary)();
@@ -199,6 +234,8 @@ const MIRROR_TOOLS = [
199
234
  { name: 'analytics', description: 'Show learning analytics: task count, success rate', fn: toolAnalytics },
200
235
  { name: 'spend', description: 'Show today\'s token cost and spend by provider', fn: toolSpend },
201
236
  { name: 'memory_show', description: 'Show conversation memory facts and recent history', fn: toolMemoryShow },
237
+ { name: 'memory_store', description: 'Persist a fact or preference to permanent memory (records.jsonl) right now. Pass { fact: "..." }', fn: toolMemoryStore },
238
+ { name: 'memory_forget', description: 'Remove a fact or preference from permanent memory (records.jsonl). Pass { fact: "thing to forget" }', fn: toolMemoryForget },
202
239
  { name: 'lessons', description: 'Show permanent failure rules learned from past tasks', fn: toolLessons },
203
240
  { name: 'skills_list', description: 'List all loaded skills with descriptions', fn: toolSkillsList },
204
241
  { name: 'tools_list', description: 'List all registered tool names', fn: toolToolsList },