clementine-agent 1.0.97 → 1.0.99

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.
@@ -59,8 +59,15 @@ export declare class SelfImproveLoop {
59
59
  private parseJsonResponse;
60
60
  private withTimeout;
61
61
  }
62
+ /** Validate that a proposed change has valid syntax for its target area. */
62
63
  export declare function validateProposal(area: string, target: string, proposedChange: string): {
63
64
  valid: boolean;
64
65
  error?: string;
65
66
  };
67
+ /**
68
+ * Resolve the on-disk path for a prompt-override `target` string.
69
+ * Accepted forms: 'global', 'agent:<slug>', 'job:<jobName>'. Returns null
70
+ * for malformed targets.
71
+ */
72
+ export declare function promptOverridePathForTarget(target: string): string | null;
66
73
  //# sourceMappingURL=self-improve.d.ts.map
@@ -11,9 +11,10 @@
11
11
  import { randomBytes } from 'node:crypto';
12
12
  import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync, } from 'node:fs';
13
13
  import matter from 'gray-matter';
14
+ import { load as yamlLoad } from 'js-yaml';
14
15
  import path from 'node:path';
15
16
  import pino from 'pino';
16
- import { BASE_DIR, SELF_IMPROVE_DIR, SOUL_FILE, AGENTS_FILE, CRON_FILE, WORKFLOWS_DIR, VAULT_DIR, MEMORY_DB_PATH, AGENTS_DIR, PKG_DIR, CRON_REFLECTIONS_DIR, GOALS_DIR, } from '../config.js';
17
+ import { BASE_DIR, SELF_IMPROVE_DIR, SOUL_FILE, AGENTS_FILE, CRON_FILE, WORKFLOWS_DIR, VAULT_DIR, MEMORY_DB_PATH, AGENTS_DIR, CRON_REFLECTIONS_DIR, GOALS_DIR, } from '../config.js';
17
18
  import { listAllGoals } from '../tools/shared.js';
18
19
  const logger = pino({ name: 'clementine.self-improve' });
19
20
  // ── Defaults ─────────────────────────────────────────────────────────
@@ -23,9 +24,13 @@ const DEFAULT_CONFIG = {
23
24
  maxDurationMs: 3_600_000, // 1 hour
24
25
  acceptThreshold: 0.7,
25
26
  plateauLimit: 3,
26
- // 'source' deprecated — self-improvement should produce data (cron, workflow, etc.),
27
- // not engine TS edits. Re-add only with CLEMENTINE_ALLOW_SOURCE_EDITS=1.
28
- areas: ['soul', 'cron', 'workflow', 'memory', 'agent', 'communication', 'goal'],
27
+ // 'source' deprecated — self-improvement produces data, not engine TS edits.
28
+ // 'advisor-rule' writes YAML to ~/.clementine/advisor-rules/user/.
29
+ // 'prompt-override' writes markdown to ~/.clementine/prompt-overrides/.
30
+ areas: [
31
+ 'soul', 'cron', 'workflow', 'memory', 'agent', 'communication', 'goal',
32
+ 'advisor-rule', 'prompt-override',
33
+ ],
29
34
  autoApply: true,
30
35
  sourceMode: 'skip',
31
36
  };
@@ -132,20 +137,22 @@ function checkDrift(proposedContent) {
132
137
  return { ok: similarity >= DRIFT_SIMILARITY_THRESHOLD, similarity };
133
138
  }
134
139
  /** Classify the risk level of a proposed change.
135
- * - low: agent prompts, individual cron job prompts auto-apply safe
140
+ * - low: agent prompts, individual cron job prompts, advisor rules, prompt overrides
136
141
  * - medium: SOUL.md, AGENTS.md, MEMORY.md — needs owner approval
137
- * - high: source code — stays blocked
142
+ * - high: source code — stays blocked (deprecated path; kept for back-compat)
138
143
  */
139
144
  function classifyRisk(area) {
140
145
  switch (area) {
141
- case 'agent': return 'low'; // Agent-scoped, easily reversible
142
- case 'cron': return 'low'; // Cron prompt tweaks, low blast radius
143
- case 'workflow': return 'low'; // Workflow definitions, scoped
146
+ case 'agent': return 'low';
147
+ case 'cron': return 'low';
148
+ case 'workflow': return 'low';
149
+ case 'advisor-rule': return 'low'; // YAML files, hot-reloaded, easily deleted
150
+ case 'prompt-override': return 'low'; // Markdown files, hot-reloaded, easily deleted
144
151
  case 'soul': return 'medium'; // Core personality — needs approval
145
152
  case 'communication': return 'medium'; // Global operating instructions
146
153
  case 'memory': return 'medium'; // Memory config
147
- case 'source': return 'high'; // Code changes — always blocked in auto mode
148
154
  case 'goal': return 'medium'; // New goals need owner review before activating
155
+ case 'source': return 'high'; // Deprecated — quarantined in Phase 1
149
156
  default: return 'high';
150
157
  }
151
158
  }
@@ -836,7 +843,15 @@ export class SelfImproveLoop {
836
843
  `Area notes:\n` +
837
844
  `- For "goal": target = "{owner}/{goal-slug}" (e.g. "clementine/improve-reply-rates" or "ross-the-sdr/book-demos"). ` +
838
845
  `Propose when you observe a pattern in completed tasks or cron runs that suggests a missing or stale goal. ` +
839
- `The proposedChange must be a JSON goal object with at minimum: title, description, priority, reviewFrequency.\n\n` +
846
+ `The proposedChange must be a JSON goal object with at minimum: title, description, priority, reviewFrequency.\n` +
847
+ `- For "advisor-rule": target = ruleId in kebab-case (e.g. "skip-turn-bump-on-unleashed"). ` +
848
+ `Use when the fix is a behavioral rule that affects ALL jobs matching some scope, not just one cron job. ` +
849
+ `Examples: "for unleashed jobs, never bump maxTurns" or "for ross-the-sdr, double timeout on max_turns". ` +
850
+ `The proposedChange must be a full advisor rule YAML body with: schemaVersion: 1, id (must match target), description, priority (use 100+ to override builtins), appliesTo, when[], then[]. ` +
851
+ `User rules at priority 100+ override engine builtins of the same id.\n` +
852
+ `- For "prompt-override": target = "global", "agent:<slug>", or "job:<jobName>" (e.g. "job:market-leader-followup"). ` +
853
+ `Use when a job/agent needs more standing guidance — markdown that gets prepended to its prompt. ` +
854
+ `The proposedChange is the markdown body (optionally with gray-matter frontmatter for priority/position).\n\n` +
840
855
  `Return your answer as a JSON object matching the schema: { "results": [ ... ] }. Up to 3 items. If absolutely nothing actionable today, return { "results": [] }.`;
841
856
  const analysisResult = await this.assistant.runPlanStep('si-analyze', analysisPrompt, {
842
857
  tier: 2,
@@ -915,10 +930,6 @@ export class SelfImproveLoop {
915
930
  const agentFile = path.join(AGENTS_DIR, target, 'agent.md');
916
931
  return existsSync(agentFile) ? readFileSync(agentFile, 'utf-8') : '';
917
932
  }
918
- case 'source': {
919
- const srcFile = path.join(PKG_DIR, 'src', target);
920
- return existsSync(srcFile) ? readFileSync(srcFile, 'utf-8') : '';
921
- }
922
933
  case 'communication':
923
934
  return existsSync(AGENTS_FILE) ? readFileSync(AGENTS_FILE, 'utf-8') : '';
924
935
  case 'memory': {
@@ -946,6 +957,23 @@ export class SelfImproveLoop {
946
957
  return '(no goals yet for this owner)';
947
958
  return goals.map((g) => `[${g.status ?? 'unknown'}] ${g.title}: ${(g.description ?? '').slice(0, 120)}`).join('\n');
948
959
  }
960
+ case 'advisor-rule': {
961
+ // target = ruleId (kebab-case). Show user override file if present, else builtin.
962
+ const userPath = path.join(BASE_DIR, 'advisor-rules', 'user', `${target}.yaml`);
963
+ if (existsSync(userPath))
964
+ return readFileSync(userPath, 'utf-8');
965
+ const builtinPath = path.join(BASE_DIR, 'advisor-rules', 'builtin', `${target}.yaml`);
966
+ if (existsSync(builtinPath))
967
+ return readFileSync(builtinPath, 'utf-8');
968
+ return '(no existing advisor rule for this id — proposing a new one)';
969
+ }
970
+ case 'prompt-override': {
971
+ // target = 'global' | 'agent:<slug>' | 'job:<jobName>'
972
+ const filePath = promptOverridePathForTarget(target);
973
+ if (filePath && existsSync(filePath))
974
+ return readFileSync(filePath, 'utf-8');
975
+ return '(no existing prompt override for this scope — proposing a new one)';
976
+ }
949
977
  default:
950
978
  return '';
951
979
  }
@@ -1011,38 +1039,10 @@ export class SelfImproveLoop {
1011
1039
  if (!targetPath) {
1012
1040
  return `Cannot resolve target path for area=${pending.area}, target=${pending.target}`;
1013
1041
  }
1014
- // Route source changes through the safe pipeline
1042
+ // 'source' area is deprecated (Phase 1 quarantine). Reject up-front so a
1043
+ // misbehaving proposal cannot reach the safeSourceEdit primitive.
1015
1044
  if (pending.area === 'source') {
1016
- const { safeSourceEdit } = await import('./safe-restart.js');
1017
- const result = await safeSourceEdit(PKG_DIR, [
1018
- { relativePath: `src/${pending.target}`, content: pending.proposedChange },
1019
- ], { experimentId, reason: `self-improve: ${pending.hypothesis.slice(0, 60)}`, description: pending.hypothesis });
1020
- if (!result.success) {
1021
- return `Source edit failed: ${result.error}${result.preflightErrors ? '\n' + result.preflightErrors.join('\n') : ''}`;
1022
- }
1023
- // Update experiment log — mark as approved
1024
- this.updateExperimentStatus(experimentId, 'approved');
1025
- try {
1026
- unlinkSync(pendingFile);
1027
- }
1028
- catch { /* ignore */ }
1029
- const state = this.loadState();
1030
- state.pendingApprovals = Math.max(0, state.pendingApprovals - 1);
1031
- this.saveState(state);
1032
- // Schedule impact measurement for 24h later
1033
- try {
1034
- appendFileSync(IMPACT_CHECKS_FILE, JSON.stringify({
1035
- experimentId,
1036
- area: pending.area,
1037
- target: pending.target,
1038
- appliedAt: new Date().toISOString(),
1039
- checkAfterMs: 24 * 60 * 60 * 1000,
1040
- }) + '\n');
1041
- }
1042
- catch (err) {
1043
- logger.warn({ err }, 'Failed to schedule impact check');
1044
- }
1045
- return `Applied source change to ${pending.target} — restart triggered.`;
1045
+ return 'source area is deprecated propose advisor-rule or prompt-override instead';
1046
1046
  }
1047
1047
  // Goal area: parse JSON, inject required fields, ensure parent dir exists
1048
1048
  if (pending.area === 'goal') {
@@ -1095,6 +1095,7 @@ export class SelfImproveLoop {
1095
1095
  }
1096
1096
  }
1097
1097
  // Write the change (non-source areas)
1098
+ mkdirSync(path.dirname(targetPath), { recursive: true });
1098
1099
  writeFileSync(targetPath, pending.proposedChange);
1099
1100
  // Record version for rollback lineage
1100
1101
  this.recordVersion(experimentId, pending.area, pending.target, pending.hypothesis, pending.before);
@@ -1680,9 +1681,6 @@ export class SelfImproveLoop {
1680
1681
  case 'agent': {
1681
1682
  return path.join(AGENTS_DIR, target, 'agent.md');
1682
1683
  }
1683
- case 'source': {
1684
- return path.join(PKG_DIR, 'src', target);
1685
- }
1686
1684
  case 'communication':
1687
1685
  return AGENTS_FILE;
1688
1686
  case 'memory':
@@ -1696,6 +1694,12 @@ export class SelfImproveLoop {
1696
1694
  return path.join(GOALS_DIR, `${goalSlug}.json`);
1697
1695
  return path.join(AGENTS_DIR, owner, 'goals', `${goalSlug}.json`);
1698
1696
  }
1697
+ case 'advisor-rule':
1698
+ if (!/^[a-z0-9-]+$/.test(target))
1699
+ return null;
1700
+ return path.join(BASE_DIR, 'advisor-rules', 'user', `${target}.yaml`);
1701
+ case 'prompt-override':
1702
+ return promptOverridePathForTarget(target);
1699
1703
  default:
1700
1704
  return null;
1701
1705
  }
@@ -1733,22 +1737,6 @@ export class SelfImproveLoop {
1733
1737
  }
1734
1738
  // ── Utility ──────────────────────────────────────────────────────────
1735
1739
  /** Validate that a proposed change has valid syntax for its target area. */
1736
- /** Files that must never be modified by self-improvement (catastrophic blast radius or self-referential). */
1737
- const SOURCE_BLOCKLIST = new Set([
1738
- 'config.ts',
1739
- 'types.ts',
1740
- 'gateway/router.ts',
1741
- 'gateway/lanes.ts',
1742
- 'gateway/heartbeat-scheduler.ts',
1743
- 'gateway/cron-scheduler.ts',
1744
- 'gateway/security-scanner.ts',
1745
- 'agent/self-improve.ts',
1746
- 'agent/safe-restart.ts',
1747
- 'agent/source-mods.ts',
1748
- 'cli/index.ts',
1749
- 'cli/dashboard.ts',
1750
- 'security/scanner.ts',
1751
- ]);
1752
1740
  export function validateProposal(area, target, proposedChange) {
1753
1741
  if (!proposedChange.trim()) {
1754
1742
  return { valid: false, error: 'Proposed change is empty' };
@@ -1790,16 +1778,71 @@ export function validateProposal(area, target, proposedChange) {
1790
1778
  }
1791
1779
  }
1792
1780
  if (area === 'source') {
1793
- // Check blocklist
1794
- if (SOURCE_BLOCKLIST.has(target)) {
1795
- return { valid: false, error: `Source file '${target}' is in the blocklist and cannot be modified by self-improvement` };
1781
+ // Deprecated — Phase 1 quarantined source self-edit. Reject up front so
1782
+ // a misbehaving LLM proposal doesn't even get cached.
1783
+ return { valid: false, error: 'source area is deprecated; propose advisor-rule or prompt-override instead' };
1784
+ }
1785
+ if (area === 'advisor-rule') {
1786
+ // Must parse as YAML and have schemaVersion: 1, id matching target, when[], then[].
1787
+ if (!/^[a-z0-9-]+$/.test(target)) {
1788
+ return { valid: false, error: `advisor-rule target must be kebab-case (got "${target}")` };
1789
+ }
1790
+ let parsed;
1791
+ try {
1792
+ parsed = yamlLoad(proposedChange);
1793
+ }
1794
+ catch (err) {
1795
+ return { valid: false, error: `advisor-rule YAML parse error: ${err instanceof Error ? err.message : String(err)}` };
1796
+ }
1797
+ if (!parsed || typeof parsed !== 'object') {
1798
+ return { valid: false, error: 'advisor-rule body did not parse as a YAML object' };
1799
+ }
1800
+ const r = parsed;
1801
+ if (r.schemaVersion !== 1)
1802
+ return { valid: false, error: 'advisor-rule must declare schemaVersion: 1' };
1803
+ if (typeof r.id !== 'string' || r.id !== target) {
1804
+ return { valid: false, error: `advisor-rule id must match target ("${target}")` };
1805
+ }
1806
+ if (!Array.isArray(r.when) || !Array.isArray(r.then)) {
1807
+ return { valid: false, error: 'advisor-rule must have when[] and then[] arrays' };
1808
+ }
1809
+ }
1810
+ if (area === 'prompt-override') {
1811
+ // target format: 'global' | 'agent:<slug>' | 'job:<jobName>'
1812
+ const path = promptOverridePathForTarget(target);
1813
+ if (!path) {
1814
+ return { valid: false, error: `prompt-override target must be 'global', 'agent:<slug>', or 'job:<jobName>' (got "${target}")` };
1815
+ }
1816
+ if (proposedChange.length > 20_000) {
1817
+ return { valid: false, error: 'prompt-override content exceeds 20KB sanity bound' };
1796
1818
  }
1797
- // Size sanity: reject wholesale rewrites (proposed content > 2x original would be caught by caller).
1798
- // Source proposals may be small patches or modules without import/export statements;
1799
- // callers that apply source changes do the syntax-aware validation.
1800
1819
  }
1801
1820
  return { valid: true };
1802
1821
  }
1822
+ /**
1823
+ * Resolve the on-disk path for a prompt-override `target` string.
1824
+ * Accepted forms: 'global', 'agent:<slug>', 'job:<jobName>'. Returns null
1825
+ * for malformed targets.
1826
+ */
1827
+ export function promptOverridePathForTarget(target) {
1828
+ const root = path.join(BASE_DIR, 'prompt-overrides');
1829
+ if (target === 'global')
1830
+ return path.join(root, '_global.md');
1831
+ const idx = target.indexOf(':');
1832
+ if (idx <= 0)
1833
+ return null;
1834
+ const scope = target.slice(0, idx);
1835
+ const key = target.slice(idx + 1);
1836
+ if (!key)
1837
+ return null;
1838
+ if (/[\/\\\.]/.test(key))
1839
+ return null;
1840
+ if (scope === 'agent')
1841
+ return path.join(root, 'agents', `${key}.md`);
1842
+ if (scope === 'job')
1843
+ return path.join(root, 'jobs', `${key}.md`);
1844
+ return null;
1845
+ }
1803
1846
  function ensureDirs() {
1804
1847
  for (const dir of [SELF_IMPROVE_DIR, PENDING_DIR]) {
1805
1848
  if (!existsSync(dir)) {
package/dist/types.d.ts CHANGED
@@ -501,7 +501,7 @@ export interface SelfImproveExperiment {
501
501
  startedAt: string;
502
502
  finishedAt: string;
503
503
  durationMs: number;
504
- area: 'soul' | 'cron' | 'workflow' | 'memory' | 'agent' | 'source' | 'communication' | 'goal';
504
+ area: 'soul' | 'cron' | 'workflow' | 'memory' | 'agent' | 'source' | 'communication' | 'goal' | 'advisor-rule' | 'prompt-override';
505
505
  target: string;
506
506
  hypothesis: string;
507
507
  proposedChange: string;
@@ -549,7 +549,7 @@ export interface SelfImproveConfig {
549
549
  maxDurationMs: number;
550
550
  acceptThreshold: number;
551
551
  plateauLimit: number;
552
- areas: ('soul' | 'cron' | 'workflow' | 'memory' | 'agent' | 'source' | 'communication' | 'goal')[];
552
+ areas: ('soul' | 'cron' | 'workflow' | 'memory' | 'agent' | 'source' | 'communication' | 'goal' | 'advisor-rule' | 'prompt-override')[];
553
553
  /** Enable tiered auto-apply: low-risk changes apply without approval. Default: false. */
554
554
  autoApply?: boolean;
555
555
  /** Target a specific agent slug (for per-agent improvement cycles). */
@@ -5,10 +5,19 @@
5
5
  * Each exports a `migration` object conforming to VaultMigration.
6
6
  * State is tracked in ~/.clementine/.vault-migrations.json.
7
7
  */
8
- import type { VaultMigrationSummary } from './types.js';
8
+ import type { MigrationContext, VaultMigrationSummary } from './types.js';
9
9
  /**
10
- * Run all pending vault migrations against the user's vault.
11
- * Idempotent safe to call multiple times.
10
+ * Run all pending migrations. Each is dispatched based on its shape:
11
+ * - VaultMigration (no `kind` field) apply(vaultDir)
12
+ * - Migration (has `kind` field) → apply(MigrationContext)
13
+ *
14
+ * Idempotent — safe to call multiple times. State is tracked in
15
+ * `~/.clementine/.vault-migrations.json` (kept this filename for back-compat).
16
+ */
17
+ export declare function runMigrations(ctx: MigrationContext, backupDir?: string): Promise<VaultMigrationSummary>;
18
+ /**
19
+ * Back-compat wrapper: existing call sites pass just vaultDir. Builds a
20
+ * default MigrationContext from BASE_DIR and PKG_DIR.
12
21
  */
13
22
  export declare function runVaultMigrations(vaultDir: string, backupDir?: string): Promise<VaultMigrationSummary>;
14
23
  //# sourceMappingURL=runner.d.ts.map
@@ -8,7 +8,7 @@
8
8
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, copyFileSync } from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import pino from 'pino';
11
- import { VAULT_MIGRATIONS_STATE } from '../config.js';
11
+ import { BASE_DIR, PKG_DIR, VAULT_MIGRATIONS_STATE } from '../config.js';
12
12
  const logger = pino({ name: 'clementine.vault-migrations' });
13
13
  /** Load the migration state file. Returns empty state if missing or corrupt. */
14
14
  function loadState() {
@@ -41,6 +41,7 @@ function backupFile(filePath, backupDir) {
41
41
  /**
42
42
  * Discover all migration modules in the compiled dist/vault-migrations/ directory.
43
43
  * Returns them sorted by filename (numeric prefix ensures correct order).
44
+ * Accepts both VaultMigration (legacy) and Migration (multi-target) shapes.
44
45
  */
45
46
  async function discoverMigrations() {
46
47
  const migrations = [];
@@ -64,16 +65,24 @@ async function discoverMigrations() {
64
65
  }
65
66
  }
66
67
  catch (err) {
67
- logger.warn({ file, err }, 'Failed to load vault migration');
68
+ logger.warn({ file, err }, 'Failed to load migration');
68
69
  }
69
70
  }
70
71
  return migrations;
71
72
  }
73
+ /** Discriminator — true if migration uses the new MigrationContext shape. */
74
+ function isContextMigration(m) {
75
+ return typeof m.kind === 'string';
76
+ }
72
77
  /**
73
- * Run all pending vault migrations against the user's vault.
74
- * Idempotent safe to call multiple times.
78
+ * Run all pending migrations. Each is dispatched based on its shape:
79
+ * - VaultMigration (no `kind` field) apply(vaultDir)
80
+ * - Migration (has `kind` field) → apply(MigrationContext)
81
+ *
82
+ * Idempotent — safe to call multiple times. State is tracked in
83
+ * `~/.clementine/.vault-migrations.json` (kept this filename for back-compat).
75
84
  */
76
- export async function runVaultMigrations(vaultDir, backupDir) {
85
+ export async function runMigrations(ctx, backupDir) {
77
86
  const summary = {
78
87
  applied: [],
79
88
  skipped: [],
@@ -87,16 +96,14 @@ export async function runVaultMigrations(vaultDir, backupDir) {
87
96
  if (migrations.length === 0)
88
97
  return summary;
89
98
  for (const migration of migrations) {
90
- // Skip if already recorded as applied
91
99
  if (appliedIds.has(migration.id)) {
92
100
  summary.alreadyRun.push(migration.id);
93
101
  continue;
94
102
  }
95
103
  try {
96
- // Back up target files before migration
97
- if (backupDir) {
98
- // Best-effort backup of the vault's 00-System dir (most common target)
99
- const systemDir = path.join(vaultDir, '00-System');
104
+ // Best-effort backup of the vault's 00-System dir for vault-style migrations
105
+ if (backupDir && !isContextMigration(migration)) {
106
+ const systemDir = path.join(ctx.vaultDir, '00-System');
100
107
  if (existsSync(systemDir)) {
101
108
  const systemFiles = readdirSync(systemDir).filter(f => f.endsWith('.md'));
102
109
  for (const f of systemFiles) {
@@ -104,36 +111,36 @@ export async function runVaultMigrations(vaultDir, backupDir) {
104
111
  }
105
112
  }
106
113
  }
107
- const result = migration.apply(vaultDir);
108
- if (result.applied) {
114
+ const result = isContextMigration(migration)
115
+ ? await migration.apply(ctx)
116
+ : migration.apply(ctx.vaultDir);
117
+ const r = result;
118
+ if (r.applied) {
109
119
  summary.applied.push(migration.id);
110
- state.applied.push({
111
- id: migration.id,
112
- appliedAt: new Date().toISOString(),
113
- result: 'applied',
114
- });
115
- logger.info({ id: migration.id, details: result.details }, 'Vault migration applied');
120
+ state.applied.push({ id: migration.id, appliedAt: new Date().toISOString(), result: 'applied' });
121
+ logger.info({ id: migration.id, details: r.details }, 'Migration applied');
116
122
  }
117
- else if (result.skipped) {
123
+ else if (r.skipped) {
118
124
  summary.skipped.push(migration.id);
119
- // Record as applied so we don't re-check every update
120
- state.applied.push({
121
- id: migration.id,
122
- appliedAt: new Date().toISOString(),
123
- result: 'skipped',
124
- });
125
- logger.info({ id: migration.id, details: result.details }, 'Vault migration skipped (already present)');
125
+ state.applied.push({ id: migration.id, appliedAt: new Date().toISOString(), result: 'skipped' });
126
+ logger.info({ id: migration.id, details: r.details }, 'Migration skipped (already present)');
126
127
  }
127
128
  }
128
129
  catch (err) {
129
130
  const errMsg = String(err).slice(0, 200);
130
131
  summary.failed.push(migration.id);
131
132
  summary.errors.push({ id: migration.id, error: errMsg });
132
- // Do NOT record as applied — will retry on next update
133
- logger.warn({ id: migration.id, err }, 'Vault migration failed');
133
+ logger.warn({ id: migration.id, err }, 'Migration failed');
134
134
  }
135
135
  }
136
136
  saveState(state);
137
137
  return summary;
138
138
  }
139
+ /**
140
+ * Back-compat wrapper: existing call sites pass just vaultDir. Builds a
141
+ * default MigrationContext from BASE_DIR and PKG_DIR.
142
+ */
143
+ export async function runVaultMigrations(vaultDir, backupDir) {
144
+ return runMigrations({ vaultDir, baseDir: BASE_DIR, pkgDir: PKG_DIR }, backupDir);
145
+ }
139
146
  //# sourceMappingURL=runner.js.map
@@ -1,10 +1,27 @@
1
1
  /**
2
- * Vault migration system — types and interfaces.
2
+ * Migration system — types and interfaces.
3
3
  *
4
- * Vault migrations ship structural changes to user vault files (SOUL.md,
5
- * AGENTS.md, etc.) alongside code updates. Each migration is idempotent
6
- * and runs once during `clementine update`.
4
+ * Migrations ship structural changes to user data files (SOUL.md, AGENTS.md,
5
+ * advisor rules, prompt overrides, clementine.json, etc.) alongside code
6
+ * updates. Each migration is idempotent and runs once during `clementine update`.
7
+ *
8
+ * Two shapes coexist for back-compat:
9
+ * - VaultMigration: takes vaultDir only (the original shape, used by 0001-0003)
10
+ * - Migration: takes a MigrationContext { vaultDir, baseDir, pkgDir } so a
11
+ * migration can touch advisor-rules/, prompt-overrides/, clementine.json,
12
+ * etc. — anything under ~/.clementine/, not just the vault.
13
+ *
14
+ * Discriminator: `kind` field. Vault-style migrations omit it.
7
15
  */
16
+ export interface MigrationContext {
17
+ /** ~/.clementine/vault */
18
+ vaultDir: string;
19
+ /** ~/.clementine/ (data home — config, state, cache, advisor-rules, etc.) */
20
+ baseDir: string;
21
+ /** Package install root (where dist/ lives). For migrations that ship templates. */
22
+ pkgDir: string;
23
+ }
24
+ /** Original vault-only migration shape. Existing 0001-0003 use this. */
8
25
  export interface VaultMigration {
9
26
  /** Unique ID matching the filename (e.g., "0001-add-execution-framework"). */
10
27
  id: string;
@@ -13,6 +30,15 @@ export interface VaultMigration {
13
30
  /** Apply the migration. Must be idempotent — safe to re-run. */
14
31
  apply: (vaultDir: string) => MigrationResult;
15
32
  }
33
+ /** Multi-target migration shape. Use for any data outside vault/. */
34
+ export interface Migration {
35
+ /** Discriminator. Vault-only migrations omit this and use VaultMigration. */
36
+ kind: 'vault' | 'config' | 'advisor-rules' | 'prompt-overrides' | 'multi';
37
+ id: string;
38
+ description: string;
39
+ apply: (ctx: MigrationContext) => MigrationResult | Promise<MigrationResult>;
40
+ }
41
+ export type AnyMigration = VaultMigration | Migration;
16
42
  export interface MigrationResult {
17
43
  /** True if changes were written to disk. */
18
44
  applied: boolean;
@@ -39,4 +65,6 @@ export interface VaultMigrationSummary {
39
65
  error: string;
40
66
  }>;
41
67
  }
68
+ /** Alias — same shape, generalized name now that migrations aren't vault-only. */
69
+ export type MigrationSummary = VaultMigrationSummary;
42
70
  //# sourceMappingURL=types.d.ts.map
@@ -1,9 +1,17 @@
1
1
  /**
2
- * Vault migration system — types and interfaces.
2
+ * Migration system — types and interfaces.
3
3
  *
4
- * Vault migrations ship structural changes to user vault files (SOUL.md,
5
- * AGENTS.md, etc.) alongside code updates. Each migration is idempotent
6
- * and runs once during `clementine update`.
4
+ * Migrations ship structural changes to user data files (SOUL.md, AGENTS.md,
5
+ * advisor rules, prompt overrides, clementine.json, etc.) alongside code
6
+ * updates. Each migration is idempotent and runs once during `clementine update`.
7
+ *
8
+ * Two shapes coexist for back-compat:
9
+ * - VaultMigration: takes vaultDir only (the original shape, used by 0001-0003)
10
+ * - Migration: takes a MigrationContext { vaultDir, baseDir, pkgDir } so a
11
+ * migration can touch advisor-rules/, prompt-overrides/, clementine.json,
12
+ * etc. — anything under ~/.clementine/, not just the vault.
13
+ *
14
+ * Discriminator: `kind` field. Vault-style migrations omit it.
7
15
  */
8
16
  export {};
9
17
  //# sourceMappingURL=types.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.0.97",
3
+ "version": "1.0.99",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",