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.
- package/dist/agent/agent-manager.js +32 -0
- package/dist/agent/assistant.js +8 -22
- package/dist/agent/daily-planner.js +5 -15
- package/dist/agent/insight-engine.js +4 -14
- package/dist/agent/prompt-evolver.js +7 -15
- package/dist/agent/self-improve.js +20 -26
- package/dist/agent/strategic-planner.js +5 -26
- package/dist/agent/team-bus.js +5 -14
- package/dist/cli/dashboard.js +2 -3
- package/dist/cli/routes/digest.d.ts +0 -1
- package/dist/cli/routes/digest.js +6 -11
- package/dist/cli/routes/goals.d.ts +5 -2
- package/dist/cli/routes/goals.js +85 -74
- package/dist/gateway/cron-scheduler.js +31 -20
- package/dist/gateway/heartbeat-scheduler.d.ts +3 -5
- package/dist/gateway/heartbeat-scheduler.js +8 -16
- package/dist/tools/goal-tools.d.ts +4 -2
- package/dist/tools/goal-tools.js +46 -58
- package/dist/tools/session-tools.js +14 -21
- package/dist/tools/shared.d.ts +40 -0
- package/dist/tools/shared.js +84 -0
- package/package.json +1 -1
package/dist/cli/routes/goals.js
CHANGED
|
@@ -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,
|
|
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 {
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
return null;
|
|
67
|
+
catch { /* ignore */ }
|
|
75
68
|
}
|
|
76
|
-
|
|
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
|
-
|
|
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
|
|
113
|
-
if (!
|
|
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
|
-
|
|
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
|
|
151
|
-
if (!
|
|
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(
|
|
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
|
|
166
|
-
if (!
|
|
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
|
|
217
|
-
if (!
|
|
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(
|
|
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
|
|
1422
|
-
if (!
|
|
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
|
|
1427
|
-
|
|
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
|
|
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
|
|
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
|
|
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)}
|
|
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)}
|
|
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
|
|
1603
|
-
|
|
1604
|
-
|
|
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
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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
|
|
800
|
-
*
|
|
801
|
-
*
|
|
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
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
|
5
|
-
*
|
|
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;
|