clementine-agent 1.0.11 → 1.0.13

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.
@@ -1,79 +1,73 @@
1
1
  /**
2
- * Goals API routes — extracted from dashboard.ts
2
+ * Goals API routes — extracted from dashboard.ts.
3
+ *
4
+ * Goals live per-owner: Clementine's at ~/.clementine/goals/, each agent's at
5
+ * ~/.clementine/vault/00-System/agents/{slug}/goals/. Uses the goal-store
6
+ * helpers in tools/shared.ts so the dashboard doesn't need to know the layout.
3
7
  */
4
8
  import { Router } from 'express';
5
9
  import express from 'express';
6
- import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync, unlinkSync, } from 'node:fs';
10
+ import { existsSync, readFileSync, writeFileSync, readdirSync, unlinkSync, } from 'node:fs';
7
11
  import path from 'node:path';
8
12
  import matter from 'gray-matter';
13
+ import { listAllGoals, findGoalPath, readGoalById, writeGoalForOwner } from '../../tools/shared.js';
9
14
  export function goalsRouter(deps) {
10
15
  const router = Router();
11
- const { goalsDir, cronRunsDir, vaultDir, cronFile, getGateway } = deps;
16
+ const { cronRunsDir, vaultDir, cronFile, getGateway } = deps;
12
17
  // List goals with contributions + delegations
13
18
  router.get('/progress', (_req, res) => {
14
- if (!existsSync(goalsDir)) {
15
- res.json({ goals: [] });
16
- return;
17
- }
18
19
  try {
19
- const files = readdirSync(goalsDir).filter(f => f.endsWith('.json'));
20
- const goals = files.map(f => {
21
- try {
22
- const goal = JSON.parse(readFileSync(path.join(goalsDir, f), 'utf-8'));
23
- const agentContributions = {};
24
- if (goal.linkedCronJobs?.length && existsSync(cronRunsDir)) {
25
- for (const jobName of goal.linkedCronJobs) {
26
- const safe = jobName.replace(/[^a-zA-Z0-9_-]/g, '_');
27
- const logFile = path.join(cronRunsDir, `${safe}.jsonl`);
28
- if (!existsSync(logFile))
20
+ const goals = listAllGoals().map(({ goal, owner }) => {
21
+ const agentContributions = {};
22
+ if (goal.linkedCronJobs?.length && existsSync(cronRunsDir)) {
23
+ for (const jobName of goal.linkedCronJobs) {
24
+ const safe = jobName.replace(/[^a-zA-Z0-9_-]/g, '_');
25
+ const logFile = path.join(cronRunsDir, `${safe}.jsonl`);
26
+ if (!existsSync(logFile))
27
+ continue;
28
+ const lines = readFileSync(logFile, 'utf-8').trim().split('\n').filter(Boolean);
29
+ for (const line of lines.slice(-20)) {
30
+ try {
31
+ const entry = JSON.parse(line);
32
+ const agent = entry.agentSlug || jobName;
33
+ if (!agentContributions[agent])
34
+ agentContributions[agent] = { runs: 0, successes: 0 };
35
+ agentContributions[agent].runs++;
36
+ if (entry.status === 'ok')
37
+ agentContributions[agent].successes++;
38
+ agentContributions[agent].lastRun = entry.finishedAt;
39
+ }
40
+ catch {
29
41
  continue;
30
- const lines = readFileSync(logFile, 'utf-8').trim().split('\n').filter(Boolean);
31
- for (const line of lines.slice(-20)) {
32
- try {
33
- const entry = JSON.parse(line);
34
- const agent = entry.agentSlug || jobName;
35
- if (!agentContributions[agent])
36
- agentContributions[agent] = { runs: 0, successes: 0 };
37
- agentContributions[agent].runs++;
38
- if (entry.status === 'ok')
39
- agentContributions[agent].successes++;
40
- agentContributions[agent].lastRun = entry.finishedAt;
41
- }
42
- catch {
43
- continue;
44
- }
45
42
  }
46
43
  }
47
44
  }
48
- const delegationsDir = path.join(vaultDir, '00-System', 'agents');
49
- const delegations = [];
50
- if (existsSync(delegationsDir)) {
51
- try {
52
- for (const agentDir of readdirSync(delegationsDir)) {
53
- const tasksDir = path.join(delegationsDir, agentDir, 'delegations');
54
- if (!existsSync(tasksDir))
55
- continue;
56
- for (const tf of readdirSync(tasksDir).filter(tf => tf.endsWith('.json'))) {
57
- try {
58
- const task = JSON.parse(readFileSync(path.join(tasksDir, tf), 'utf-8'));
59
- if (task.goalId === goal.id) {
60
- delegations.push({ agent: task.toAgent || agentDir, task: task.task || tf, status: task.status || 'pending' });
61
- }
62
- }
63
- catch {
64
- continue;
45
+ }
46
+ const delegationsDir = path.join(vaultDir, '00-System', 'agents');
47
+ const delegations = [];
48
+ if (existsSync(delegationsDir)) {
49
+ try {
50
+ for (const agentDir of readdirSync(delegationsDir)) {
51
+ const tasksDir = path.join(delegationsDir, agentDir, 'delegations');
52
+ if (!existsSync(tasksDir))
53
+ continue;
54
+ for (const tf of readdirSync(tasksDir).filter(tf => tf.endsWith('.json'))) {
55
+ try {
56
+ const task = JSON.parse(readFileSync(path.join(tasksDir, tf), 'utf-8'));
57
+ if (task.goalId === goal.id) {
58
+ delegations.push({ agent: task.toAgent || agentDir, task: task.task || tf, status: task.status || 'pending' });
65
59
  }
66
60
  }
61
+ catch {
62
+ continue;
63
+ }
67
64
  }
68
65
  }
69
- catch { /* ignore */ }
70
66
  }
71
- return { ...goal, agentContributions, delegations };
72
- }
73
- catch {
74
- return null;
67
+ catch { /* ignore */ }
75
68
  }
76
- }).filter(Boolean);
69
+ return { ...goal, owner, agentContributions, delegations };
70
+ });
77
71
  res.json({ goals });
78
72
  }
79
73
  catch {
@@ -83,8 +77,6 @@ export function goalsRouter(deps) {
83
77
  // Create goal
84
78
  router.post('/', express.json(), (req, res) => {
85
79
  try {
86
- if (!existsSync(goalsDir))
87
- mkdirSync(goalsDir, { recursive: true });
88
80
  const id = Math.random().toString(16).slice(2, 10);
89
81
  const { title, description, owner, priority, status, targetDate, linkedCronJobs, nextActions, blockers, reviewFrequency } = req.body;
90
82
  if (!title) {
@@ -99,7 +91,7 @@ export function goalsRouter(deps) {
99
91
  reviewFrequency: reviewFrequency || 'weekly', linkedCronJobs: linkedCronJobs || [],
100
92
  targetDate: targetDate || undefined,
101
93
  };
102
- writeFileSync(path.join(goalsDir, `${id}.json`), JSON.stringify(goal, null, 2));
94
+ writeGoalForOwner(goal);
103
95
  res.json({ ok: true, goal });
104
96
  }
105
97
  catch (e) {
@@ -109,19 +101,21 @@ export function goalsRouter(deps) {
109
101
  // Update goal
110
102
  router.put('/:id', express.json(), (req, res) => {
111
103
  try {
112
- const goalPath = path.join(goalsDir, `${req.params.id}.json`);
113
- if (!existsSync(goalPath)) {
104
+ const found = findGoalPath(req.params.id);
105
+ if (!found) {
106
+ res.status(404).json({ ok: false, error: 'Goal not found' });
107
+ return;
108
+ }
109
+ const existing = readGoalById(req.params.id);
110
+ if (!existing) {
114
111
  res.status(404).json({ ok: false, error: 'Goal not found' });
115
112
  return;
116
113
  }
117
- const existing = JSON.parse(readFileSync(goalPath, 'utf-8'));
118
114
  const { title, description, owner, priority, status, targetDate, linkedCronJobs, nextActions, blockers, reviewFrequency } = req.body;
119
115
  if (title !== undefined)
120
116
  existing.title = title;
121
117
  if (description !== undefined)
122
118
  existing.description = description;
123
- if (owner !== undefined)
124
- existing.owner = owner;
125
119
  if (priority !== undefined)
126
120
  existing.priority = priority;
127
121
  if (status !== undefined)
@@ -137,7 +131,21 @@ export function goalsRouter(deps) {
137
131
  if (reviewFrequency !== undefined)
138
132
  existing.reviewFrequency = reviewFrequency;
139
133
  existing.updatedAt = new Date().toISOString();
140
- writeFileSync(goalPath, JSON.stringify(existing, null, 2));
134
+ // If owner changed, re-route to the new owner's dir and remove from old location.
135
+ // Write the new copy first so we don't lose the goal if unlink fails.
136
+ if (owner !== undefined && owner !== found.owner) {
137
+ existing.owner = owner;
138
+ writeGoalForOwner(existing);
139
+ try {
140
+ unlinkSync(found.filePath);
141
+ }
142
+ catch (unlinkErr) {
143
+ console.warn(`[goals] Failed to remove old goal file ${found.filePath} after owner change to ${owner}:`, unlinkErr);
144
+ }
145
+ }
146
+ else {
147
+ writeFileSync(found.filePath, JSON.stringify(existing, null, 2));
148
+ }
141
149
  res.json({ ok: true, goal: existing });
142
150
  }
143
151
  catch (e) {
@@ -147,12 +155,12 @@ export function goalsRouter(deps) {
147
155
  // Delete goal
148
156
  router.delete('/:id', (_req, res) => {
149
157
  try {
150
- const goalPath = path.join(goalsDir, `${_req.params.id}.json`);
151
- if (!existsSync(goalPath)) {
158
+ const found = findGoalPath(_req.params.id);
159
+ if (!found) {
152
160
  res.status(404).json({ ok: false, error: 'Goal not found' });
153
161
  return;
154
162
  }
155
- unlinkSync(goalPath);
163
+ unlinkSync(found.filePath);
156
164
  res.json({ ok: true });
157
165
  }
158
166
  catch (e) {
@@ -162,12 +170,11 @@ export function goalsRouter(deps) {
162
170
  // Generate cron proposals from goal
163
171
  router.post('/:id/generate-crons', async (req, res) => {
164
172
  try {
165
- const goalPath = path.join(goalsDir, `${req.params.id}.json`);
166
- if (!existsSync(goalPath)) {
173
+ const goal = readGoalById(req.params.id);
174
+ if (!goal) {
167
175
  res.status(404).json({ ok: false, error: 'Goal not found' });
168
176
  return;
169
177
  }
170
- const goal = JSON.parse(readFileSync(goalPath, 'utf-8'));
171
178
  const prompt = `You are analyzing a goal and proposing automated scheduled tasks (cron jobs) to make progress on it.
172
179
 
173
180
  ## Goal: ${goal.title}
@@ -213,12 +220,16 @@ Respond ONLY with valid JSON:
213
220
  // Approve cron proposals
214
221
  router.post('/:id/approve-crons', express.json(), (req, res) => {
215
222
  try {
216
- const goalPath = path.join(goalsDir, `${req.params.id}.json`);
217
- if (!existsSync(goalPath)) {
223
+ const found = findGoalPath(req.params.id);
224
+ if (!found) {
225
+ res.status(404).json({ ok: false, error: 'Goal not found' });
226
+ return;
227
+ }
228
+ const goal = readGoalById(req.params.id);
229
+ if (!goal) {
218
230
  res.status(404).json({ ok: false, error: 'Goal not found' });
219
231
  return;
220
232
  }
221
- const goal = JSON.parse(readFileSync(goalPath, 'utf-8'));
222
233
  const crons = req.body.crons || [];
223
234
  if (crons.length === 0) {
224
235
  res.status(400).json({ ok: false, error: 'No crons to approve' });
@@ -245,7 +256,7 @@ Respond ONLY with valid JSON:
245
256
  goal.linkedCronJobs.push(name);
246
257
  }
247
258
  goal.updatedAt = new Date().toISOString();
248
- writeFileSync(goalPath, JSON.stringify(goal, null, 2));
259
+ writeFileSync(found.filePath, JSON.stringify(goal, null, 2));
249
260
  }
250
261
  res.json({ ok: true, added, skipped: crons.length - added.length });
251
262
  }
@@ -13,6 +13,7 @@ import cron from 'node-cron';
13
13
  import matter from 'gray-matter';
14
14
  import pino from 'pino';
15
15
  import { CRON_FILE, WORKFLOWS_DIR, AGENTS_DIR, DAILY_NOTES_DIR, BASE_DIR, DISCORD_OWNER_ID, GOALS_DIR, CRON_REFLECTIONS_DIR, ADVISOR_LOG_PATH, TIMEZONE, } from '../config.js';
16
+ import { listAllGoals, findGoalPath, readGoalById } from '../tools/shared.js';
16
17
  import { scanner } from '../security/scanner.js';
17
18
  import { parseAllWorkflows as parseAllWorkflowsSync } from '../agent/workflow-runner.js';
18
19
  import { SelfImproveLoop } from '../agent/self-improve.js';
@@ -1154,6 +1155,13 @@ export class CronScheduler {
1154
1155
  const trimmed = response.trim();
1155
1156
  if (trimmed === '__NOTHING__')
1156
1157
  return true;
1158
+ // Bare "NOTHING" (with or without underscores/parenthetical), matching heartbeat scheduler.
1159
+ if (/^_*NOTHING_*\s*(\(|$)/im.test(trimmed))
1160
+ return true;
1161
+ // Goal-work [MONITORING] classification with no substantive body —
1162
+ // Clementine checked the goal and nothing changed, so suppress the notification.
1163
+ if (/^(_*NOTHING_*\s*)?\[MONITORING\]\s*$/i.test(trimmed))
1164
+ return true;
1157
1165
  // Only treat as noise if the response is short — avoids filtering out
1158
1166
  // substantive responses that happen to start with "No updates, but..."
1159
1167
  if (trimmed.length > 80)
@@ -1418,13 +1426,14 @@ export class CronScheduler {
1418
1426
  unlinkSync(filePath);
1419
1427
  if (!trigger.goalId)
1420
1428
  continue;
1421
- const goalPath = path.join(GOALS_DIR, `${trigger.goalId}.json`);
1422
- if (!existsSync(goalPath)) {
1429
+ const found = findGoalPath(trigger.goalId);
1430
+ if (!found) {
1423
1431
  logger.warn({ goalId: trigger.goalId }, 'Goal trigger references missing goal — skipping');
1424
1432
  continue;
1425
1433
  }
1426
- const goal = JSON.parse(readFileSync(goalPath, 'utf-8'));
1427
- if (goal.status !== 'active')
1434
+ const goalPath = found.filePath;
1435
+ const goal = readGoalById(trigger.goalId);
1436
+ if (!goal || goal.status !== 'active')
1428
1437
  continue;
1429
1438
  logger.info({ goalId: trigger.goalId, title: goal.title, focus: trigger.focus }, 'Processing goal work trigger');
1430
1439
  // Load recent progress outcomes so the agent has context about what it already did
@@ -1455,14 +1464,14 @@ export class CronScheduler {
1455
1464
  const prompt = `You are working on a focused goal session.\n\n` +
1456
1465
  `## Goal: ${goal.title}\n${goal.description}\n\n` +
1457
1466
  `## Focus for this session\n${trigger.focus}\n\n` +
1458
- (goal.progressNotes?.length > 0
1467
+ (goal.progressNotes && goal.progressNotes.length > 0
1459
1468
  ? `## Prior progress\n${goal.progressNotes.slice(-5).map((n) => `- ${n}`).join('\n')}\n\n`
1460
1469
  : '') +
1461
1470
  recentOutcomesContext +
1462
- (goal.nextActions?.length > 0
1471
+ (goal.nextActions && goal.nextActions.length > 0
1463
1472
  ? `## Planned next actions\n${goal.nextActions.map((a) => `- ${a}`).join('\n')}\n\n`
1464
1473
  : '') +
1465
- (goal.blockers?.length > 0
1474
+ (goal.blockers && goal.blockers.length > 0
1466
1475
  ? `## Current blockers\n${goal.blockers.map((b) => `- ${b}`).join('\n')}\n\n`
1467
1476
  : '') +
1468
1477
  `## Instructions\n` +
@@ -1477,8 +1486,16 @@ export class CronScheduler {
1477
1486
  ` - [MONITORING] — checked status, nothing changed, will check again later\n` +
1478
1487
  `5. Keep your output concise — summarize what you accomplished.`;
1479
1488
  const jobName = `goal:${goal.title.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 40)}`;
1480
- const goalSnapshotUpdatedAt = goal.updatedAt;
1489
+ const goalSnapshotUpdatedAt = goal.updatedAt ?? '';
1481
1490
  const goalSnapshotNotes = goal.progressNotes?.length ?? 0;
1491
+ // Route goal work to the owning agent so the session runs with their
1492
+ // tools/model and output lands in their Discord channel. Clementine-owned
1493
+ // goals continue to run under Clementine's identity.
1494
+ const ownerSlug = (found.owner && found.owner !== 'clementine') ? found.owner : null;
1495
+ const dispatchOpts = ownerSlug ? { agentSlug: ownerSlug } : undefined;
1496
+ if (ownerSlug) {
1497
+ this.gateway.setSessionProfile(`cron:${jobName}`, ownerSlug);
1498
+ }
1482
1499
  // ── Route through execution advisor (same path as regular cron jobs) ──
1483
1500
  // Creates a synthetic CronJobDefinition so the advisor can apply circuit
1484
1501
  // breakers, turn-limit adjustments, model upgrades, and unleashed escalation.
@@ -1490,6 +1507,7 @@ export class CronScheduler {
1490
1507
  tier: 2,
1491
1508
  maxTurns: trigger.maxTurns ?? 15,
1492
1509
  mode: 'standard',
1510
+ ...(ownerSlug ? { agentSlug: ownerSlug } : {}),
1493
1511
  };
1494
1512
  import('../agent/execution-advisor.js').then(({ getExecutionAdvice }) => {
1495
1513
  const advice = getExecutionAdvice(jobName, syntheticJob);
@@ -1513,7 +1531,7 @@ export class CronScheduler {
1513
1531
  this.gateway.handleCronJob(jobName, enrichedPrompt, 2, effectiveMaxTurns, effectiveModel, undefined, // workDir
1514
1532
  useUnleashed ? 'unleashed' : undefined, useUnleashed ? 1 : undefined).then((result) => {
1515
1533
  if (result && !CronScheduler.isCronNoise(result)) {
1516
- this.dispatcher.send(`🎯 **Goal work: ${goal.title}**\n\n${result.slice(0, 1500)}`).catch(err => logger.debug({ err }, 'Failed to send goal work notification'));
1534
+ this.dispatcher.send(`🎯 **Goal work: ${goal.title}**\n\n${result.slice(0, 1500)}`, dispatchOpts).catch(err => logger.debug({ err }, 'Failed to send goal work notification'));
1517
1535
  }
1518
1536
  logToDailyNote(`**Goal work: ${goal.title}** — ${(result || 'completed').slice(0, 100).replace(/\n/g, ' ')}`);
1519
1537
  this.logGoalOutcome(trigger.goalId, goalPath, goalSnapshotUpdatedAt, goalSnapshotNotes, trigger.focus, trigger.source, result);
@@ -1526,7 +1544,7 @@ export class CronScheduler {
1526
1544
  logger.warn({ err, goalId: trigger.goalId }, 'Advisor unavailable — running goal work with defaults');
1527
1545
  this.gateway.handleCronJob(jobName, prompt, 2, trigger.maxTurns ?? 15).then((result) => {
1528
1546
  if (result && !CronScheduler.isCronNoise(result)) {
1529
- this.dispatcher.send(`🎯 **Goal work: ${goal.title}**\n\n${result.slice(0, 1500)}`).catch(err => logger.debug({ err }, 'Failed to send goal work notification'));
1547
+ this.dispatcher.send(`🎯 **Goal work: ${goal.title}**\n\n${result.slice(0, 1500)}`, dispatchOpts).catch(err => logger.debug({ err }, 'Failed to send goal work notification'));
1530
1548
  }
1531
1549
  logToDailyNote(`**Goal work: ${goal.title}** — ${(result || 'completed').slice(0, 100).replace(/\n/g, ' ')}`);
1532
1550
  this.logGoalOutcome(trigger.goalId, goalPath, goalSnapshotUpdatedAt, goalSnapshotNotes, trigger.focus, trigger.source, result);
@@ -1597,17 +1615,10 @@ export class CronScheduler {
1597
1615
  return;
1598
1616
  try {
1599
1617
  // Only apply suggestions linked to high-priority autoSchedule goals
1600
- const goalFiles = existsSync(GOALS_DIR) ? readdirSync(GOALS_DIR).filter(f => f.endsWith('.json')) : [];
1601
1618
  const autoScheduleGoalTitles = new Set();
1602
- for (const f of goalFiles) {
1603
- try {
1604
- const goal = JSON.parse(readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
1605
- if (goal.status === 'active' && goal.priority === 'high' && goal.autoSchedule) {
1606
- autoScheduleGoalTitles.add(goal.title.toLowerCase());
1607
- }
1608
- }
1609
- catch {
1610
- continue;
1619
+ for (const { goal } of listAllGoals()) {
1620
+ if (goal.status === 'active' && goal.priority === 'high' && goal.autoSchedule) {
1621
+ autoScheduleGoalTitles.add(String(goal.title).toLowerCase());
1611
1622
  }
1612
1623
  }
1613
1624
  if (autoScheduleGoalTitles.size === 0)
@@ -47,11 +47,9 @@ export declare class HeartbeatScheduler {
47
47
  */
48
48
  private getRecentActivitySummary;
49
49
  /**
50
- * Read and parse all goal JSON files from GOALS_DIR once. Callers that
51
- * need filtered subsets (active only, priority-based, etc.) do their own
52
- * filtering over the returned array. Used by heartbeatTick to avoid
53
- * repeating the readdirSync+readFileSync pass for every goal-consuming
54
- * method.
50
+ * Read and parse all goal JSON files across Clementine's global goals dir
51
+ * AND every per-agent goals dir. Callers that need filtered subsets
52
+ * (active only, priority-based, etc.) do their own filtering.
55
53
  */
56
54
  static loadAllGoals(): Array<any>;
57
55
  /**
@@ -10,6 +10,7 @@ import path from 'node:path';
10
10
  import matter from 'gray-matter';
11
11
  import pino from 'pino';
12
12
  import { HEARTBEAT_FILE, TASKS_FILE, INBOX_DIR, DAILY_NOTES_DIR, HEARTBEAT_INTERVAL_MINUTES, HEARTBEAT_ACTIVE_START, HEARTBEAT_ACTIVE_END, BASE_DIR, GOALS_DIR, HEARTBEAT_WORK_QUEUE_FILE, DISCORD_OWNER_ID, } from '../config.js';
13
+ import { listAllGoals } from '../tools/shared.js';
13
14
  import { gatherInsightSignals, buildInsightPrompt, parseInsightResponse, canSendInsight, recordInsightSent, recordInsightAcked, maybeIncreaseCooldown, } from '../agent/insight-engine.js';
14
15
  import { CronRunLog, logToDailyNote, todayISO } from './cron-scheduler.js';
15
16
  const logger = pino({ name: 'clementine.heartbeat' });
@@ -796,25 +797,16 @@ export class HeartbeatScheduler {
796
797
  return lines.join('\n');
797
798
  }
798
799
  /**
799
- * Read and parse all goal JSON files from GOALS_DIR once. Callers that
800
- * need filtered subsets (active only, priority-based, etc.) do their own
801
- * filtering over the returned array. Used by heartbeatTick to avoid
802
- * repeating the readdirSync+readFileSync pass for every goal-consuming
803
- * method.
800
+ * Read and parse all goal JSON files across Clementine's global goals dir
801
+ * AND every per-agent goals dir. Callers that need filtered subsets
802
+ * (active only, priority-based, etc.) do their own filtering.
804
803
  */
805
804
  static loadAllGoals() {
806
805
  try {
807
- if (!existsSync(GOALS_DIR))
808
- return [];
809
- const files = readdirSync(GOALS_DIR).filter(f => f.endsWith('.json'));
810
- return files
811
- .map(f => { try {
812
- return JSON.parse(readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
813
- }
814
- catch {
815
- return null;
816
- } })
817
- .filter((g) => g !== null);
806
+ return listAllGoals().map(({ goal, owner }) => ({
807
+ ...goal,
808
+ owner: goal.owner || owner,
809
+ }));
818
810
  }
819
811
  catch (err) {
820
812
  logger.warn({ err }, 'loadAllGoals failed');
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Clementine TypeScript — Goal MCP tools.
3
3
  *
4
- * Persistent goals that drive proactive agent behavior and
5
- * can be linked to cron jobs for autonomous progress.
4
+ * Persistent goals that survive across sessions and drive proactive behavior.
5
+ * Goals live per-owner: Clementine's at ~/.clementine/goals/, each agent's at
6
+ * ~/.clementine/vault/00-System/agents/{slug}/goals/. Helpers in shared.ts
7
+ * handle routing so tools here don't need to know the layout.
6
8
  */
7
9
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
10
  export declare function registerGoalTools(server: McpServer): void;