clementine-agent 1.18.135 → 1.18.136

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.
@@ -31,7 +31,7 @@ const DEFAULT_CONFIG = {
31
31
  // 'prompt-override' writes markdown to ~/.clementine/prompt-overrides/.
32
32
  areas: [
33
33
  'soul', 'cron', 'workflow', 'memory', 'agent', 'communication', 'goal',
34
- 'advisor-rule', 'prompt-override',
34
+ 'advisor-rule', 'prompt-override', 'skill',
35
35
  ],
36
36
  autoApply: true,
37
37
  sourceMode: 'skip',
@@ -150,6 +150,13 @@ function classifyRisk(area) {
150
150
  case 'workflow': return 'low';
151
151
  case 'advisor-rule': return 'low'; // YAML files, hot-reloaded, easily deleted
152
152
  case 'prompt-override': return 'low'; // Markdown files, hot-reloaded, easily deleted
153
+ // 1.18.136 — 'skill' is the new first-class self-improve target.
154
+ // Skills are user-facing recipes that fire across many tasks; a
155
+ // bad change can cascade into every cron that pins them. Medium
156
+ // tier so changes always surface for owner approval, never auto-
157
+ // apply. Frontmatter + Anthropic spec are validated before any
158
+ // proposal can even reach the queue (see validateProposal).
159
+ case 'skill': return 'medium';
153
160
  case 'soul': return 'medium'; // Core personality — needs approval
154
161
  case 'communication': return 'medium'; // Global operating instructions
155
162
  case 'memory': return 'medium'; // Memory config
@@ -1140,7 +1147,11 @@ export class SelfImproveLoop {
1140
1147
  `User rules at priority 100+ override engine builtins of the same id.\n` +
1141
1148
  `- For "prompt-override": target = "global", "agent:<slug>", or "job:<jobName>" (e.g. "job:daily-summary"). ` +
1142
1149
  `Use when a job/agent needs more standing guidance — markdown that gets prepended to its prompt. ` +
1143
- `The proposedChange is the markdown body (optionally with gray-matter frontmatter for priority/position).\n\n` +
1150
+ `The proposedChange is the markdown body (optionally with gray-matter frontmatter for priority/position).\n` +
1151
+ `- For "skill": target = the exact skill slug (matches Anthropic regex ^[a-z0-9][a-z0-9-]{0,63}$, e.g. "morning-briefing"). ` +
1152
+ `Use when a skill is producing low-quality output, has a high failure rate, OR is stale (>90 days unused but still pinned to a cron). ` +
1153
+ `Surface candidates: skills with low clementine.useCount + recent run failures, OR skills whose linked cron jobs appear in the failing-jobs list above. ` +
1154
+ `The proposedChange must be the FULL SKILL.md (frontmatter + body): preserve the "name" field exactly, keep "description" ≤1024 chars and free of XML tags, body changes only — Clementine will preserve the clementine.useCount/createdAt/version block on save so do NOT include or alter that block.\n\n` +
1144
1155
  `Return your answer as a JSON object matching the schema: { "results": [ ... ] }. Up to 3 items. If absolutely nothing actionable today, return { "results": [] }.`;
1145
1156
  const analysisResult = await this.assistant.runPlanStep('si-analyze', analysisPrompt, {
1146
1157
  tier: 2,
@@ -1200,7 +1211,11 @@ export class SelfImproveLoop {
1200
1211
  `- Generate a SPECIFIC, MINIMAL change (not a full rewrite)\n` +
1201
1212
  `- Explain WHY this change should improve the metric\n` +
1202
1213
  `- IMPORTANT: "proposedChange" must be the COMPLETE updated file content (not just the diff), because it will replace the entire file\n` +
1203
- `- For source code changes: preserve all imports, exports, and function signatures. Only modify implementation details.\n\n` +
1214
+ `- For source code changes: preserve all imports, exports, and function signatures. Only modify implementation details.\n` +
1215
+ (selected.area === 'skill'
1216
+ ? `- For skill changes: keep the frontmatter "name" and "description" intact (description ≤1024 chars, no XML tags). Modify the BODY (procedure section after the frontmatter). Do NOT include or alter the "clementine:" frontmatter block — Clementine preserves useCount/createdAt/version automatically on save.\n`
1217
+ : '') +
1218
+ `\n` +
1204
1219
  `Output ONLY a JSON object (no markdown, no explanation):\n` +
1205
1220
  `{ "area": "${selected.area}", "target": "${selected.target}", "hypothesis": "what will improve and why", "proposedChange": "the complete updated file content with your minimal change applied" }`;
1206
1221
  const result = await this.assistant.runPlanStep('si-hypothesize', proposalPrompt, {
@@ -1217,6 +1232,20 @@ export class SelfImproveLoop {
1217
1232
  return existsSync(SOUL_FILE) ? readFileSync(SOUL_FILE, 'utf-8') : '';
1218
1233
  case 'cron':
1219
1234
  return existsSync(CRON_FILE) ? readFileSync(CRON_FILE, 'utf-8') : '';
1235
+ case 'skill': {
1236
+ // 1.18.136 — read SKILL.md from the folder-form path. Fall back
1237
+ // to the flat-form (legacy) path if no folder. The hypothesizer
1238
+ // proposes against the BODY only — frontmatter is preserved on
1239
+ // write by validateProposal + writeSkill (see resolveTargetPath
1240
+ // / applyApprovedChange below).
1241
+ const folderEntry = path.join(VAULT_DIR, '00-System', 'skills', target, 'SKILL.md');
1242
+ const flatEntry = path.join(VAULT_DIR, '00-System', 'skills', target + '.md');
1243
+ if (existsSync(folderEntry))
1244
+ return readFileSync(folderEntry, 'utf-8');
1245
+ if (existsSync(flatEntry))
1246
+ return readFileSync(flatEntry, 'utf-8');
1247
+ return '';
1248
+ }
1220
1249
  case 'workflow': {
1221
1250
  const wfFile = path.join(WORKFLOWS_DIR, target.endsWith('.md') ? target : `${target}.md`);
1222
1251
  return existsSync(wfFile) ? readFileSync(wfFile, 'utf-8') : '';
@@ -1389,6 +1418,70 @@ export class SelfImproveLoop {
1389
1418
  return `Cannot apply change — identity drift too high (similarity: ${drift.similarity.toFixed(3)}, threshold: ${DRIFT_SIMILARITY_THRESHOLD})`;
1390
1419
  }
1391
1420
  }
1421
+ // Skill area: merge proposed frontmatter with existing clementine.*
1422
+ // lifecycle block so an LLM-authored proposal doesn't clobber
1423
+ // useCount / createdAt / version. The proposal supplies the new
1424
+ // name/description/body; the lifecycle metadata stays Clementine's
1425
+ // responsibility. This sits between validation and the generic
1426
+ // writeFileSync below so every other area still writes raw.
1427
+ if (pending.area === 'skill') {
1428
+ try {
1429
+ const proposed = matter(pending.proposedChange);
1430
+ const existing = pending.before ? matter(pending.before) : null;
1431
+ const proposedFm = (proposed.data ?? {});
1432
+ const existingFm = (existing?.data ?? {});
1433
+ const existingExt = (existingFm.clementine ?? {});
1434
+ const proposedExt = (proposedFm.clementine ?? {});
1435
+ const now = new Date().toISOString();
1436
+ const mergedExt = {
1437
+ ...existingExt,
1438
+ // Allow proposal to update tools.allow / triggers / source if it
1439
+ // explicitly sets them, but preserve lifecycle counters from
1440
+ // the existing record.
1441
+ ...proposedExt,
1442
+ useCount: existingExt.useCount ?? 0,
1443
+ createdAt: existingExt.createdAt ?? now,
1444
+ updatedAt: now,
1445
+ version: typeof existingExt.version === 'number' ? existingExt.version + 1 : 1,
1446
+ lastSelfImproveExperimentId: experimentId,
1447
+ };
1448
+ const mergedFm = {
1449
+ name: proposedFm.name ?? existingFm.name,
1450
+ description: proposedFm.description ?? existingFm.description,
1451
+ ...(proposedFm.title || existingFm.title ? { title: proposedFm.title ?? existingFm.title } : {}),
1452
+ clementine: mergedExt,
1453
+ };
1454
+ const body = proposed.content.endsWith('\n') ? proposed.content : proposed.content + '\n';
1455
+ const merged = matter.stringify(body, mergedFm);
1456
+ mkdirSync(path.dirname(targetPath), { recursive: true });
1457
+ writeFileSync(targetPath, merged);
1458
+ // Record version + finish the standard apply flow below.
1459
+ this.recordVersion(experimentId, pending.area, pending.target, pending.hypothesis, pending.before);
1460
+ logger.info({ id: experimentId, area: pending.area, target: pending.target }, 'Applied approved skill change (lifecycle preserved)');
1461
+ this.updateExperimentStatus(experimentId, 'approved');
1462
+ try {
1463
+ unlinkSync(pendingFile);
1464
+ }
1465
+ catch { /* ignore */ }
1466
+ const stateAfter = this.loadState();
1467
+ stateAfter.pendingApprovals = Math.max(0, stateAfter.pendingApprovals - 1);
1468
+ this.saveState(stateAfter);
1469
+ try {
1470
+ appendFileSync(IMPACT_CHECKS_FILE, JSON.stringify({
1471
+ experimentId,
1472
+ area: pending.area,
1473
+ target: pending.target,
1474
+ appliedAt: new Date().toISOString(),
1475
+ checkAfterMs: 24 * 60 * 60 * 1000,
1476
+ }) + '\n');
1477
+ }
1478
+ catch { /* ignore impact-check schedule errors */ }
1479
+ return `Applied skill change to ${path.relative(VAULT_DIR, targetPath)} (useCount preserved, version bumped)`;
1480
+ }
1481
+ catch (err) {
1482
+ return `Cannot apply skill change — frontmatter merge failed: ${err instanceof Error ? err.message : String(err)}`;
1483
+ }
1484
+ }
1392
1485
  // Write the change (non-source areas)
1393
1486
  mkdirSync(path.dirname(targetPath), { recursive: true });
1394
1487
  writeFileSync(targetPath, pending.proposedChange);
@@ -2009,6 +2102,21 @@ export class SelfImproveLoop {
2009
2102
  return SOUL_FILE;
2010
2103
  case 'cron':
2011
2104
  return CRON_FILE;
2105
+ case 'skill': {
2106
+ // 1.18.136 — skill changes route through writeSkill (not raw
2107
+ // writeFileSync) so frontmatter validation runs and the existing
2108
+ // useCount/lastUsed/createdAt are preserved. The intercept lives
2109
+ // in applyApprovedChange below; this still returns the canonical
2110
+ // path so the version recorder + impact-check writer have
2111
+ // something to anchor on.
2112
+ const folderEntry = path.join(VAULT_DIR, '00-System', 'skills', target, 'SKILL.md');
2113
+ const flatEntry = path.join(VAULT_DIR, '00-System', 'skills', target + '.md');
2114
+ if (existsSync(folderEntry))
2115
+ return folderEntry;
2116
+ if (existsSync(flatEntry))
2117
+ return flatEntry;
2118
+ return null;
2119
+ }
2012
2120
  case 'workflow': {
2013
2121
  const name = target.endsWith('.md') ? target : `${target}.md`;
2014
2122
  return path.join(WORKFLOWS_DIR, name);
@@ -2117,6 +2225,52 @@ export function validateProposal(area, target, proposedChange) {
2117
2225
  // a misbehaving LLM proposal doesn't even get cached.
2118
2226
  return { valid: false, error: 'source area is deprecated; propose advisor-rule or prompt-override instead' };
2119
2227
  }
2228
+ if (area === 'skill') {
2229
+ // 1.18.136 — skill body validation. The proposedChange is the FULL
2230
+ // SKILL.md (frontmatter + body). Anthropic spec rules: name must
2231
+ // match ^[a-z0-9][a-z0-9-]{0,63}$, description ≤1024 chars, body
2232
+ // present + non-empty, no XML tags in description, no Anthropic-
2233
+ // reserved words in name. Reuses the centralized validator that
2234
+ // dashboard + MCP + auto-extract all share.
2235
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(target)) {
2236
+ return { valid: false, error: `skill target must be a valid slug (got "${target}")` };
2237
+ }
2238
+ let parsed;
2239
+ try {
2240
+ parsed = matter(proposedChange);
2241
+ }
2242
+ catch (err) {
2243
+ return { valid: false, error: `skill SKILL.md frontmatter parse error: ${err instanceof Error ? err.message : String(err)}` };
2244
+ }
2245
+ const fm = parsed.data;
2246
+ const name = typeof fm.name === 'string' ? fm.name : '';
2247
+ const description = typeof fm.description === 'string' ? fm.description : '';
2248
+ const body = parsed.content || '';
2249
+ if (!name || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(name)) {
2250
+ return { valid: false, error: 'skill frontmatter "name" missing or invalid slug' };
2251
+ }
2252
+ if (name !== target) {
2253
+ return { valid: false, error: `skill frontmatter "name" (${name}) must match target slug (${target})` };
2254
+ }
2255
+ if (/\b(anthropic|claude)\b/i.test(name)) {
2256
+ return { valid: false, error: 'skill name uses a reserved word (anthropic/claude)' };
2257
+ }
2258
+ if (!description.trim()) {
2259
+ return { valid: false, error: 'skill frontmatter "description" required' };
2260
+ }
2261
+ if (description.length > 1024) {
2262
+ return { valid: false, error: `skill description ${description.length} chars exceeds Anthropic spec ceiling of 1024` };
2263
+ }
2264
+ if (/<\w+/.test(description)) {
2265
+ return { valid: false, error: 'skill description must not contain XML tags' };
2266
+ }
2267
+ if (!body.trim()) {
2268
+ return { valid: false, error: 'skill body (procedure) required' };
2269
+ }
2270
+ // Body line count is a SOFT limit (warning, not error) — keeps
2271
+ // validation strict but doesn't block proposals that legitimately
2272
+ // need more lines.
2273
+ }
2120
2274
  if (area === 'advisor-rule') {
2121
2275
  // Must parse as YAML and have schemaVersion: 1, id matching target, when[], then[].
2122
2276
  if (!/^[a-z0-9-]+$/.test(target)) {
package/dist/types.d.ts CHANGED
@@ -957,7 +957,7 @@ export interface SelfImproveExperiment {
957
957
  startedAt: string;
958
958
  finishedAt: string;
959
959
  durationMs: number;
960
- area: 'soul' | 'cron' | 'workflow' | 'memory' | 'agent' | 'source' | 'communication' | 'goal' | 'advisor-rule' | 'prompt-override';
960
+ area: 'soul' | 'cron' | 'workflow' | 'memory' | 'agent' | 'source' | 'communication' | 'goal' | 'advisor-rule' | 'prompt-override' | 'skill';
961
961
  target: string;
962
962
  hypothesis: string;
963
963
  proposedChange: string;
@@ -1014,7 +1014,7 @@ export interface SelfImproveConfig {
1014
1014
  */
1015
1015
  surfaceThreshold?: number;
1016
1016
  plateauLimit: number;
1017
- areas: ('soul' | 'cron' | 'workflow' | 'memory' | 'agent' | 'source' | 'communication' | 'goal' | 'advisor-rule' | 'prompt-override')[];
1017
+ areas: ('soul' | 'cron' | 'workflow' | 'memory' | 'agent' | 'source' | 'communication' | 'goal' | 'advisor-rule' | 'prompt-override' | 'skill')[];
1018
1018
  /** Enable tiered auto-apply: low-risk changes apply without approval. Default: false. */
1019
1019
  autoApply?: boolean;
1020
1020
  /** Target a specific agent slug (for per-agent improvement cycles). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.135",
3
+ "version": "1.18.136",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",