clementine-agent 1.0.11 → 1.0.12
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/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/agent/assistant.js
CHANGED
|
@@ -13,11 +13,11 @@ import fs from 'node:fs';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { query as rawQuery, listSubagents, getSubagentMessages, } from '@anthropic-ai/claude-agent-sdk';
|
|
15
15
|
import pino from 'pino';
|
|
16
|
-
import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, PROFILES_DIR, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SEARCH_CONTEXT_LIMIT, SEARCH_RECENCY_LIMIT, SYSTEM_PROMPT_MAX_CONTEXT_CHARS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, UNLEASHED_PHASE_TURNS, UNLEASHED_DEFAULT_MAX_HOURS, UNLEASHED_MAX_PHASES, PROJECTS_META_FILE,
|
|
16
|
+
import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, PROFILES_DIR, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SEARCH_CONTEXT_LIMIT, SEARCH_RECENCY_LIMIT, SYSTEM_PROMPT_MAX_CONTEXT_CHARS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, UNLEASHED_PHASE_TURNS, UNLEASHED_DEFAULT_MAX_HOURS, UNLEASHED_MAX_PHASES, PROJECTS_META_FILE, CRON_PROGRESS_DIR, CRON_REFLECTIONS_DIR, HANDOFFS_DIR, BUDGET, ENABLE_1M_CONTEXT, IDENTITY_FILE, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, } from '../config.js';
|
|
17
17
|
import { DEFAULT_CHANNEL_CAPABILITIES } from '../types.js';
|
|
18
18
|
import { enforceToolPermissions, getSecurityPrompt, getHeartbeatSecurityPrompt, getCronSecurityPrompt, getHeartbeatDisallowedTools, logToolUse, setProfileTier, setProfileAllowedTools, setAgentDir, setSendPolicy, setInteractionSource, } from './hooks.js';
|
|
19
19
|
import { scanner } from '../security/scanner.js';
|
|
20
|
-
import { agentWorkingMemoryFile } from '../tools/shared.js';
|
|
20
|
+
import { agentWorkingMemoryFile, listAllGoals } from '../tools/shared.js';
|
|
21
21
|
import { AgentManager } from './agent-manager.js';
|
|
22
22
|
import { extractLinks } from './link-extractor.js';
|
|
23
23
|
import { StallGuard } from './stall-guard.js';
|
|
@@ -1491,17 +1491,9 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1491
1491
|
}
|
|
1492
1492
|
const goals = [];
|
|
1493
1493
|
try {
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
try {
|
|
1498
|
-
const goal = JSON.parse(fs.readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
|
|
1499
|
-
if (goal.status === 'active')
|
|
1500
|
-
goals.push({ goal, file: f });
|
|
1501
|
-
}
|
|
1502
|
-
catch {
|
|
1503
|
-
continue;
|
|
1504
|
-
}
|
|
1494
|
+
for (const { goal, filePath } of listAllGoals()) {
|
|
1495
|
+
if (goal.status === 'active')
|
|
1496
|
+
goals.push({ goal, file: path.basename(filePath) });
|
|
1505
1497
|
}
|
|
1506
1498
|
}
|
|
1507
1499
|
catch { /* non-fatal */ }
|
|
@@ -3039,15 +3031,9 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3039
3031
|
// ── Goal context: inject linked goal info ───────────────────────
|
|
3040
3032
|
let goalContext = '';
|
|
3041
3033
|
try {
|
|
3042
|
-
|
|
3043
|
-
const
|
|
3044
|
-
|
|
3045
|
-
.map(f => { try {
|
|
3046
|
-
return JSON.parse(fs.readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
|
|
3047
|
-
}
|
|
3048
|
-
catch {
|
|
3049
|
-
return null;
|
|
3050
|
-
} })
|
|
3034
|
+
{
|
|
3035
|
+
const linkedGoals = listAllGoals()
|
|
3036
|
+
.map(({ goal }) => goal)
|
|
3051
3037
|
.filter(g => g && g.status === 'active' && g.linkedCronJobs?.includes(jobName));
|
|
3052
3038
|
if (linkedGoals.length > 0) {
|
|
3053
3039
|
const goalLines = linkedGoals.map((g) => {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import pino from 'pino';
|
|
11
|
-
import { BASE_DIR,
|
|
11
|
+
import { BASE_DIR, CRON_REFLECTIONS_DIR, TASKS_FILE, INBOX_DIR, MODELS, } from '../config.js';
|
|
12
|
+
import { listAllGoals } from '../tools/shared.js';
|
|
12
13
|
const logger = pino({ name: 'clementine.daily-planner' });
|
|
13
14
|
const PLANS_DIR = path.join(BASE_DIR, 'plans', 'daily');
|
|
14
15
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
@@ -95,21 +96,10 @@ export class DailyPlanner {
|
|
|
95
96
|
return sections.join('\n\n') || 'No context available — all clear.';
|
|
96
97
|
}
|
|
97
98
|
loadActiveGoals() {
|
|
98
|
-
if (!existsSync(GOALS_DIR))
|
|
99
|
-
return [];
|
|
100
99
|
try {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
const goal = JSON.parse(readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
|
|
106
|
-
if (goal.status === 'active')
|
|
107
|
-
goals.push(goal);
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
100
|
+
const goals = listAllGoals()
|
|
101
|
+
.map(({ goal }) => goal)
|
|
102
|
+
.filter(g => g && g.status === 'active');
|
|
113
103
|
return goals.sort((a, b) => {
|
|
114
104
|
const p = { high: 0, medium: 1, low: 2 };
|
|
115
105
|
return (p[a.priority] ?? 2) - (p[b.priority] ?? 2);
|
|
@@ -12,6 +12,7 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import pino from 'pino';
|
|
14
14
|
import { GOALS_DIR, BASE_DIR } from '../config.js';
|
|
15
|
+
import { listAllGoals } from '../tools/shared.js';
|
|
15
16
|
const logger = pino({ name: 'clementine.insight-engine' });
|
|
16
17
|
const BASE_COOLDOWN_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
17
18
|
const MAX_DAILY_INSIGHTS = 3;
|
|
@@ -133,20 +134,9 @@ export function gatherInsightSignals(gateway) {
|
|
|
133
134
|
if (sessionCount === 0) {
|
|
134
135
|
// No recent activity — could note quiet period if there are pending goals
|
|
135
136
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
const g = JSON.parse(readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
|
|
141
|
-
return g.status === 'active' && g.priority === 'high';
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
if (activeHighPriority.length > 0) {
|
|
148
|
-
signals.push(`Quiet period: ${activeHighPriority.length} high-priority goal(s) active but no recent user interaction`);
|
|
149
|
-
}
|
|
137
|
+
const activeHighPriority = listAllGoals().filter(({ goal }) => goal.status === 'active' && goal.priority === 'high');
|
|
138
|
+
if (activeHighPriority.length > 0) {
|
|
139
|
+
signals.push(`Quiet period: ${activeHighPriority.length} high-priority goal(s) active but no recent user interaction`);
|
|
150
140
|
}
|
|
151
141
|
}
|
|
152
142
|
catch { /* non-fatal */ }
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* reflections, progress state, and goal context. Returns the enrichment
|
|
6
6
|
* string to be appended to the original prompt.
|
|
7
7
|
*/
|
|
8
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync
|
|
8
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import pino from 'pino';
|
|
11
|
-
import { BASE_DIR, CRON_REFLECTIONS_DIR, CRON_PROGRESS_DIR
|
|
11
|
+
import { BASE_DIR, CRON_REFLECTIONS_DIR, CRON_PROGRESS_DIR } from '../config.js';
|
|
12
|
+
import { listAllGoals } from '../tools/shared.js';
|
|
12
13
|
const logger = pino({ name: 'clementine.prompt-evolver' });
|
|
13
14
|
/**
|
|
14
15
|
* Evolve a static cron prompt by enriching it with lessons from reflections,
|
|
@@ -225,22 +226,13 @@ function extractProgressInsights(jobName) {
|
|
|
225
226
|
* Find goals that reference this cron job and inject alignment guidance.
|
|
226
227
|
*/
|
|
227
228
|
function extractGoalGuidance(jobName) {
|
|
228
|
-
if (!existsSync(GOALS_DIR))
|
|
229
|
-
return null;
|
|
230
229
|
try {
|
|
231
|
-
const files = readdirSync(GOALS_DIR).filter(f => f.endsWith('.json'));
|
|
232
230
|
const linkedGoals = [];
|
|
233
|
-
for (const
|
|
234
|
-
|
|
235
|
-
const goal = JSON.parse(readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
|
|
236
|
-
if (goal.status !== 'active')
|
|
237
|
-
continue;
|
|
238
|
-
if (goal.linkedCronJobs?.includes(jobName)) {
|
|
239
|
-
linkedGoals.push(goal);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
231
|
+
for (const { goal } of listAllGoals()) {
|
|
232
|
+
if (goal.status !== 'active')
|
|
243
233
|
continue;
|
|
234
|
+
if (goal.linkedCronJobs?.includes(jobName)) {
|
|
235
|
+
linkedGoals.push(goal);
|
|
244
236
|
}
|
|
245
237
|
}
|
|
246
238
|
if (linkedGoals.length === 0)
|
|
@@ -14,6 +14,7 @@ import matter from 'gray-matter';
|
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import pino from 'pino';
|
|
16
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 { listAllGoals } from '../tools/shared.js';
|
|
17
18
|
const logger = pino({ name: 'clementine.self-improve' });
|
|
18
19
|
// ── Defaults ─────────────────────────────────────────────────────────
|
|
19
20
|
const DEFAULT_CONFIG = {
|
|
@@ -481,34 +482,27 @@ export class SelfImproveLoop {
|
|
|
481
482
|
}
|
|
482
483
|
}
|
|
483
484
|
catch { /* non-fatal */ }
|
|
484
|
-
// Gather goal health data
|
|
485
|
+
// Gather goal health data (walks global + per-agent goals dirs)
|
|
485
486
|
const goalHealth = [];
|
|
486
487
|
try {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
isStale: goal.status === 'active' && daysSinceUpdate > staleThreshold,
|
|
506
|
-
linkedCronJobs: goal.linkedCronJobs || [],
|
|
507
|
-
progressCount: goal.progressNotes?.length ?? 0,
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
catch { /* skip malformed */ }
|
|
511
|
-
}
|
|
488
|
+
const now = Date.now();
|
|
489
|
+
const DAY_MS = 86_400_000;
|
|
490
|
+
for (const { goal, owner } of listAllGoals()) {
|
|
491
|
+
const lastUpdate = goal.updatedAt ? new Date(goal.updatedAt).getTime() : 0;
|
|
492
|
+
const daysSinceUpdate = Math.floor((now - lastUpdate) / DAY_MS);
|
|
493
|
+
const staleThreshold = goal.reviewFrequency === 'daily' ? 1 : goal.reviewFrequency === 'weekly' ? 7 : 30;
|
|
494
|
+
goalHealth.push({
|
|
495
|
+
id: goal.id,
|
|
496
|
+
title: goal.title,
|
|
497
|
+
status: goal.status,
|
|
498
|
+
owner: owner,
|
|
499
|
+
priority: goal.priority,
|
|
500
|
+
daysSinceUpdate,
|
|
501
|
+
reviewFrequency: goal.reviewFrequency,
|
|
502
|
+
isStale: goal.status === 'active' && daysSinceUpdate > staleThreshold,
|
|
503
|
+
linkedCronJobs: goal.linkedCronJobs || [],
|
|
504
|
+
progressCount: goal.progressNotes?.length ?? 0,
|
|
505
|
+
});
|
|
512
506
|
}
|
|
513
507
|
}
|
|
514
508
|
catch { /* non-fatal */ }
|
|
@@ -13,6 +13,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import pino from 'pino';
|
|
15
15
|
import { BASE_DIR, GOALS_DIR, MODELS } from '../config.js';
|
|
16
|
+
import { listAllGoals } from '../tools/shared.js';
|
|
16
17
|
const logger = pino({ name: 'clementine.strategic-planner' });
|
|
17
18
|
const DAILY_PLANS_DIR = path.join(BASE_DIR, 'plans', 'daily');
|
|
18
19
|
const WEEKLY_PLANS_DIR = path.join(BASE_DIR, 'plans', 'weekly');
|
|
@@ -265,34 +266,12 @@ export class StrategicPlanner {
|
|
|
265
266
|
}).filter(Boolean);
|
|
266
267
|
}
|
|
267
268
|
loadActiveGoals() {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
.filter(f => f.endsWith('.json'))
|
|
272
|
-
.map(f => {
|
|
273
|
-
try {
|
|
274
|
-
return JSON.parse(readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
})
|
|
280
|
-
.filter((g) => g && g.status === 'active');
|
|
269
|
+
return listAllGoals()
|
|
270
|
+
.map(({ goal }) => goal)
|
|
271
|
+
.filter(g => g && g.status === 'active');
|
|
281
272
|
}
|
|
282
273
|
loadAllGoals() {
|
|
283
|
-
|
|
284
|
-
return [];
|
|
285
|
-
return readdirSync(GOALS_DIR)
|
|
286
|
-
.filter(f => f.endsWith('.json'))
|
|
287
|
-
.map(f => {
|
|
288
|
-
try {
|
|
289
|
-
return JSON.parse(readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
})
|
|
295
|
-
.filter(Boolean);
|
|
274
|
+
return listAllGoals().map(({ goal }) => goal);
|
|
296
275
|
}
|
|
297
276
|
}
|
|
298
277
|
// ── Date helpers ───────────────────────────────────────────────────────
|
package/dist/agent/team-bus.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* Logs to JSONL and optionally mirrors to a Discord channel.
|
|
6
6
|
*/
|
|
7
7
|
import { createHash, randomBytes } from 'node:crypto';
|
|
8
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync,
|
|
9
|
-
import os from 'node:os';
|
|
8
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
9
|
import path from 'node:path';
|
|
11
10
|
import pino from 'pino';
|
|
11
|
+
import { listAllGoals } from '../tools/shared.js';
|
|
12
12
|
const logger = pino({ name: 'clementine.team-bus' });
|
|
13
13
|
/** Max inter-agent message depth before rejection (anti-loop). */
|
|
14
14
|
const MAX_DEPTH = 3;
|
|
@@ -247,19 +247,10 @@ export class TeamBus {
|
|
|
247
247
|
}
|
|
248
248
|
/** Broadcast a message to all team agents (optionally scoped to a goal). */
|
|
249
249
|
async broadcast(fromSlug, content, goalId, _sessionKey) {
|
|
250
|
-
const goalsDir = path.join(process.env.CLEMENTINE_HOME || path.join(os.homedir(), '.clementine'), 'goals');
|
|
251
250
|
let targetSlugs = [];
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const goal = JSON.parse(readFileSync(path.join(goalsDir, f), 'utf-8'));
|
|
256
|
-
if (goal.id === goalId && goal.owner && goal.owner !== fromSlug) {
|
|
257
|
-
targetSlugs.push(goal.owner);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
catch {
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
251
|
+
for (const { goal, owner } of listAllGoals()) {
|
|
252
|
+
if (goal.id === goalId && owner && owner !== fromSlug) {
|
|
253
|
+
targetSlugs.push(owner);
|
|
263
254
|
}
|
|
264
255
|
}
|
|
265
256
|
const allAgents = this.teamRouter.listTeamAgents();
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -5684,15 +5684,14 @@ export async function cmdDashboard(opts) {
|
|
|
5684
5684
|
res.json(analytics);
|
|
5685
5685
|
});
|
|
5686
5686
|
// ── Goals, Delegations, Workflows, Digest — extracted routers ──
|
|
5687
|
-
const GOALS_DIR = path.join(BASE_DIR, 'goals');
|
|
5688
5687
|
const CRON_RUNS_DIR = path.join(BASE_DIR, 'cron', 'runs');
|
|
5689
5688
|
const AGENTS_BASE = path.join(VAULT_DIR, '00-System', 'agents');
|
|
5690
5689
|
const WORKFLOWS_DIR = path.join(VAULT_DIR, '00-System', 'workflows');
|
|
5691
5690
|
const WORKFLOW_RUNS_DIR = path.join(BASE_DIR, 'workflows', 'runs');
|
|
5692
|
-
app.use('/api/goals', goalsRouter({
|
|
5691
|
+
app.use('/api/goals', goalsRouter({ cronRunsDir: CRON_RUNS_DIR, vaultDir: VAULT_DIR, cronFile: CRON_FILE, getGateway }));
|
|
5693
5692
|
app.use('/api/delegations', delegationsRouter({ agentsBase: AGENTS_BASE, getGateway, broadcastEvent }));
|
|
5694
5693
|
app.use('/api/workflows', workflowsRouter({ workflowsDir: WORKFLOWS_DIR, workflowRunsDir: WORKFLOW_RUNS_DIR, agentsBase: AGENTS_BASE, getGateway, broadcastEvent, cachedAsync }));
|
|
5695
|
-
app.use('/api/digest', digestRouter({ baseDir: BASE_DIR, vaultDir: VAULT_DIR,
|
|
5694
|
+
app.use('/api/digest', digestRouter({ baseDir: BASE_DIR, vaultDir: VAULT_DIR, memoryDbPath: MEMORY_DB_PATH, parseEnvFile, getGateway, cached }));
|
|
5696
5695
|
// Voice audio route (served from digest router but needs top-level mount for /api/voice/ path)
|
|
5697
5696
|
app.get('/api/voice/audio/:hash', (req, res) => {
|
|
5698
5697
|
const hash = req.params.hash.replace(/[^a-f0-9]/g, '');
|
|
@@ -7,6 +7,7 @@ import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync, } from
|
|
|
7
7
|
import { randomBytes } from 'node:crypto';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import matter from 'gray-matter';
|
|
10
|
+
import { listAllGoals } from '../../tools/shared.js';
|
|
10
11
|
export function getDigestPrefs(prefsFile) {
|
|
11
12
|
const defaults = {
|
|
12
13
|
enabled: false,
|
|
@@ -27,7 +28,7 @@ export function getDigestPrefs(prefsFile) {
|
|
|
27
28
|
}
|
|
28
29
|
export function digestRouter(deps) {
|
|
29
30
|
const router = Router();
|
|
30
|
-
const { baseDir, vaultDir,
|
|
31
|
+
const { baseDir, vaultDir, memoryDbPath, parseEnvFile, getGateway, cached } = deps;
|
|
31
32
|
const prefsFile = path.join(baseDir, 'digest-preferences.json');
|
|
32
33
|
const voiceCacheDir = path.join(baseDir, 'cache', 'voice');
|
|
33
34
|
// Graph API helper for email
|
|
@@ -101,16 +102,10 @@ export function digestRouter(deps) {
|
|
|
101
102
|
}
|
|
102
103
|
if (secs.goals !== false) {
|
|
103
104
|
try {
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
return null;
|
|
111
|
-
} }).filter(Boolean);
|
|
112
|
-
const active = goals.filter((g) => g.status === 'active');
|
|
113
|
-
const blocked = goals.filter((g) => g.status === 'blocked');
|
|
105
|
+
{
|
|
106
|
+
const goals = listAllGoals().map(e => e.goal);
|
|
107
|
+
const active = goals.filter(g => g.status === 'active');
|
|
108
|
+
const blocked = goals.filter(g => g.status === 'blocked');
|
|
114
109
|
let goalText = `${active.length} active, ${blocked.length} blocked\n`;
|
|
115
110
|
active.slice(0, 5).forEach((g) => {
|
|
116
111
|
goalText += ` - ${g.title} [${g.priority}]`;
|
|
@@ -1,10 +1,13 @@
|
|
|
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 type { Gateway } from '../../gateway/router.js';
|
|
6
10
|
export interface GoalsRouterDeps {
|
|
7
|
-
goalsDir: string;
|
|
8
11
|
cronRunsDir: string;
|
|
9
12
|
vaultDir: string;
|
|
10
13
|
cronFile: string;
|
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;
|
package/dist/tools/goal-tools.js
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
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 { randomBytes } from 'node:crypto';
|
|
8
|
-
import { existsSync, mkdirSync,
|
|
10
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
9
11
|
import path from 'node:path';
|
|
10
12
|
import { z } from 'zod';
|
|
11
|
-
import { BASE_DIR, logger, textResult } from './shared.js';
|
|
12
|
-
const GOALS_DIR = path.join(BASE_DIR, 'goals');
|
|
13
|
+
import { BASE_DIR, logger, textResult, listAllGoals, findGoalPath, readGoalById, writeGoalForOwner, } from './shared.js';
|
|
13
14
|
const GOAL_TRIGGER_DIR = path.join(BASE_DIR, 'cron', 'goal-triggers');
|
|
14
|
-
function ensureGoalsDir() {
|
|
15
|
-
if (!existsSync(GOALS_DIR))
|
|
16
|
-
mkdirSync(GOALS_DIR, { recursive: true });
|
|
17
|
-
}
|
|
18
15
|
export function registerGoalTools(server) {
|
|
19
|
-
server.tool('goal_create', 'Create a new persistent goal that survives across sessions. Goals drive proactive agent behavior and can be linked to cron jobs.', {
|
|
16
|
+
server.tool('goal_create', 'Create a new persistent goal that survives across sessions. Goals drive proactive agent behavior and can be linked to cron jobs. Agent goals live in that agent\'s own directory.', {
|
|
20
17
|
title: z.string().describe('Short goal title'),
|
|
21
18
|
description: z.string().describe('Detailed description of what this goal aims to achieve'),
|
|
22
|
-
owner: z.string().optional().describe('Agent slug that owns this goal (default: "clementine")'),
|
|
19
|
+
owner: z.string().optional().describe('Agent slug that owns this goal (default: "clementine"). Goal file is routed to the owner\'s directory.'),
|
|
23
20
|
priority: z.enum(['high', 'medium', 'low']).optional().describe('Priority level (default: "medium")'),
|
|
24
21
|
targetDate: z.string().optional().describe('Target completion date (YYYY-MM-DD)'),
|
|
25
22
|
nextActions: z.array(z.string()).optional().describe('Initial next actions to take'),
|
|
@@ -27,7 +24,6 @@ export function registerGoalTools(server) {
|
|
|
27
24
|
linkedCronJobs: z.array(z.string()).optional().describe('Cron job names that contribute to this goal'),
|
|
28
25
|
autoSchedule: z.boolean().optional().describe('Allow the daily planner to auto-create/adjust cron jobs for this goal (default: false)'),
|
|
29
26
|
}, async ({ title, description, owner, priority, targetDate, nextActions, reviewFrequency, linkedCronJobs, autoSchedule }) => {
|
|
30
|
-
ensureGoalsDir();
|
|
31
27
|
const id = randomBytes(4).toString('hex');
|
|
32
28
|
const now = new Date().toISOString();
|
|
33
29
|
const goal = {
|
|
@@ -43,9 +39,9 @@ export function registerGoalTools(server) {
|
|
|
43
39
|
linkedCronJobs: linkedCronJobs || [],
|
|
44
40
|
...(autoSchedule ? { autoSchedule } : {}),
|
|
45
41
|
};
|
|
46
|
-
|
|
47
|
-
logger.info({ goalId: id, title }, 'Goal created');
|
|
48
|
-
return textResult(`Goal created: "${title}" (ID: ${id})`);
|
|
42
|
+
const filePath = writeGoalForOwner(goal);
|
|
43
|
+
logger.info({ goalId: id, title, owner: goal.owner, filePath }, 'Goal created');
|
|
44
|
+
return textResult(`Goal created: "${title}" (ID: ${id}) — owner: ${goal.owner}`);
|
|
49
45
|
});
|
|
50
46
|
server.tool('goal_update', 'Update an existing persistent goal — add progress notes, change status, update next actions, or add blockers.', {
|
|
51
47
|
id: z.string().describe('Goal ID'),
|
|
@@ -57,14 +53,16 @@ export function registerGoalTools(server) {
|
|
|
57
53
|
priority: z.enum(['high', 'medium', 'low']).optional().describe('Change priority'),
|
|
58
54
|
autoSchedule: z.boolean().optional().describe('Allow the daily planner to auto-create/adjust cron jobs for this goal'),
|
|
59
55
|
}, async ({ id, status, progressNote, nextActions, blockers, linkedCronJobs, priority, autoSchedule }) => {
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
56
|
+
const found = findGoalPath(id);
|
|
57
|
+
if (!found)
|
|
58
|
+
return textResult(`Goal not found: ${id}`);
|
|
59
|
+
const goal = readGoalById(id);
|
|
60
|
+
if (!goal)
|
|
62
61
|
return textResult(`Goal not found: ${id}`);
|
|
63
|
-
const goal = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
64
62
|
if (status)
|
|
65
63
|
goal.status = status;
|
|
66
64
|
if (progressNote)
|
|
67
|
-
goal.progressNotes.push(`[${new Date().toISOString().slice(0, 16)}] ${progressNote}`);
|
|
65
|
+
(goal.progressNotes ||= []).push(`[${new Date().toISOString().slice(0, 16)}] ${progressNote}`);
|
|
68
66
|
if (nextActions)
|
|
69
67
|
goal.nextActions = nextActions;
|
|
70
68
|
if (blockers)
|
|
@@ -76,42 +74,32 @@ export function registerGoalTools(server) {
|
|
|
76
74
|
if (autoSchedule !== undefined)
|
|
77
75
|
goal.autoSchedule = autoSchedule;
|
|
78
76
|
goal.updatedAt = new Date().toISOString();
|
|
79
|
-
writeFileSync(filePath, JSON.stringify(goal, null, 2));
|
|
80
|
-
logger.info({ goalId: id, status: goal.status }, 'Goal updated');
|
|
77
|
+
writeFileSync(found.filePath, JSON.stringify(goal, null, 2));
|
|
78
|
+
logger.info({ goalId: id, status: goal.status, owner: found.owner }, 'Goal updated');
|
|
81
79
|
return textResult(`Goal "${goal.title}" updated (status: ${goal.status})`);
|
|
82
80
|
});
|
|
83
|
-
server.tool('goal_list', 'List persistent goals, optionally filtered by owner or status.', {
|
|
81
|
+
server.tool('goal_list', 'List persistent goals, optionally filtered by owner or status. Walks Clementine\'s global goals dir plus every agent\'s goals dir.', {
|
|
84
82
|
owner: z.string().optional().describe('Filter by owner agent slug'),
|
|
85
83
|
status: z.enum(['active', 'paused', 'completed', 'blocked']).optional().describe('Filter by status'),
|
|
86
84
|
}, async ({ owner, status }) => {
|
|
87
|
-
|
|
88
|
-
const files = readdirSync(GOALS_DIR).filter(f => f.endsWith('.json'));
|
|
89
|
-
let goals = files.map(f => {
|
|
90
|
-
try {
|
|
91
|
-
return JSON.parse(readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
}).filter(Boolean);
|
|
85
|
+
let entries = listAllGoals();
|
|
97
86
|
if (owner)
|
|
98
|
-
|
|
87
|
+
entries = entries.filter(e => e.owner === owner);
|
|
99
88
|
if (status)
|
|
100
|
-
|
|
101
|
-
if (
|
|
89
|
+
entries = entries.filter(e => e.goal.status === status);
|
|
90
|
+
if (entries.length === 0)
|
|
102
91
|
return textResult('No goals found matching the criteria.');
|
|
103
|
-
const lines =
|
|
104
|
-
const nextAct =
|
|
105
|
-
const linked =
|
|
106
|
-
return `- [${
|
|
92
|
+
const lines = entries.map(({ goal, owner: goalOwner }) => {
|
|
93
|
+
const nextAct = goal.nextActions?.length ? ` | Next: ${goal.nextActions[0]}` : '';
|
|
94
|
+
const linked = goal.linkedCronJobs?.length ? ` | Crons: ${goal.linkedCronJobs.join(', ')}` : '';
|
|
95
|
+
return `- [${String(goal.status).toUpperCase()}] **${goal.title}** (${goal.id}) — ${goal.priority} priority, owner: ${goalOwner}${nextAct}${linked}`;
|
|
107
96
|
});
|
|
108
|
-
return textResult(`Goals (${
|
|
97
|
+
return textResult(`Goals (${entries.length}):\n${lines.join('\n')}`);
|
|
109
98
|
});
|
|
110
99
|
server.tool('goal_get', 'Get a single persistent goal with full history — progress notes, next actions, blockers, and linked cron jobs.', { id: z.string().describe('Goal ID') }, async ({ id }) => {
|
|
111
|
-
const
|
|
112
|
-
if (!
|
|
100
|
+
const goal = readGoalById(id);
|
|
101
|
+
if (!goal)
|
|
113
102
|
return textResult(`Goal not found: ${id}`);
|
|
114
|
-
const goal = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
115
103
|
const sections = [
|
|
116
104
|
`# ${goal.title}`,
|
|
117
105
|
`**ID:** ${goal.id} | **Status:** ${goal.status} | **Priority:** ${goal.priority} | **Owner:** ${goal.owner}`,
|
|
@@ -119,29 +107,28 @@ export function registerGoalTools(server) {
|
|
|
119
107
|
`**Review:** ${goal.reviewFrequency}`,
|
|
120
108
|
`\n## Description\n${goal.description}`,
|
|
121
109
|
];
|
|
122
|
-
if (goal.progressNotes?.length
|
|
123
|
-
sections.push(`\n## Progress Notes\n${goal.progressNotes.map(
|
|
124
|
-
if (goal.nextActions?.length
|
|
125
|
-
sections.push(`\n## Next Actions\n${goal.nextActions.map(
|
|
126
|
-
if (goal.blockers?.length
|
|
127
|
-
sections.push(`\n## Blockers\n${goal.blockers.map(
|
|
128
|
-
if (goal.linkedCronJobs?.length
|
|
129
|
-
sections.push(`\n## Linked Cron Jobs\n${goal.linkedCronJobs.map(
|
|
110
|
+
if (goal.progressNotes?.length)
|
|
111
|
+
sections.push(`\n## Progress Notes\n${goal.progressNotes.map(n => `- ${n}`).join('\n')}`);
|
|
112
|
+
if (goal.nextActions?.length)
|
|
113
|
+
sections.push(`\n## Next Actions\n${goal.nextActions.map(a => `- [ ] ${a}`).join('\n')}`);
|
|
114
|
+
if (goal.blockers?.length)
|
|
115
|
+
sections.push(`\n## Blockers\n${goal.blockers.map(b => `- ${b}`).join('\n')}`);
|
|
116
|
+
if (goal.linkedCronJobs?.length)
|
|
117
|
+
sections.push(`\n## Linked Cron Jobs\n${goal.linkedCronJobs.map(c => `- ${c}`).join('\n')}`);
|
|
130
118
|
return textResult(sections.join('\n'));
|
|
131
119
|
});
|
|
132
|
-
server.tool('goal_work', 'Spawn a focused background work session on a specific goal. The daemon picks up the trigger and runs a goal-directed session
|
|
120
|
+
server.tool('goal_work', 'Spawn a focused background work session on a specific goal. The daemon picks up the trigger and runs a goal-directed session as the goal\'s owner (so output lands in that agent\'s channel with that agent\'s tools). Results delivered via notifications.', {
|
|
133
121
|
goal_id: z.string().describe('ID of the goal to work on'),
|
|
134
122
|
focus: z.string().optional().describe('Specific aspect to focus on. Defaults to the goal\'s first nextAction.'),
|
|
135
123
|
max_turns: z.number().optional().default(15).describe('Max agent turns for this work session'),
|
|
136
124
|
}, async ({ goal_id, focus, max_turns }) => {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (!existsSync(goalPath))
|
|
125
|
+
const goal = readGoalById(goal_id);
|
|
126
|
+
if (!goal)
|
|
140
127
|
return textResult(`Goal not found: ${goal_id}. Use goal_list to see available goals.`);
|
|
141
|
-
const goal = JSON.parse(readFileSync(goalPath, 'utf-8'));
|
|
142
128
|
if (goal.status !== 'active')
|
|
143
129
|
return textResult(`Goal "${goal.title}" is ${goal.status} — only active goals can be worked on.`);
|
|
144
|
-
|
|
130
|
+
if (!existsSync(GOAL_TRIGGER_DIR))
|
|
131
|
+
mkdirSync(GOAL_TRIGGER_DIR, { recursive: true });
|
|
145
132
|
const trigger = {
|
|
146
133
|
goalId: goal_id,
|
|
147
134
|
focus: focus || goal.nextActions?.[0] || goal.description,
|
|
@@ -150,8 +137,9 @@ export function registerGoalTools(server) {
|
|
|
150
137
|
};
|
|
151
138
|
const triggerFile = path.join(GOAL_TRIGGER_DIR, `${Date.now()}-${goal_id}.trigger.json`);
|
|
152
139
|
writeFileSync(triggerFile, JSON.stringify(trigger, null, 2));
|
|
153
|
-
logger.info({ goalId: goal_id, focus: trigger.focus }, 'Goal work session triggered');
|
|
140
|
+
logger.info({ goalId: goal_id, owner: goal.owner, focus: trigger.focus }, 'Goal work session triggered');
|
|
154
141
|
return textResult(`Triggered goal work session for "${goal.title}" (${goal_id}).\n` +
|
|
142
|
+
`Owner: ${goal.owner}\n` +
|
|
155
143
|
`Focus: ${trigger.focus}\n` +
|
|
156
144
|
`The daemon will pick it up within a few seconds. Results delivered via notifications.`);
|
|
157
145
|
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
-
import { BASE_DIR, HANDOFFS_DIR, INBOX_DIR, TASKS_FILE, logger, textResult, todayStr, } from './shared.js';
|
|
7
|
+
import { BASE_DIR, HANDOFFS_DIR, INBOX_DIR, TASKS_FILE, logger, textResult, todayStr, listAllGoals, } from './shared.js';
|
|
8
8
|
function ensureHandoffsDir() {
|
|
9
9
|
if (!existsSync(HANDOFFS_DIR))
|
|
10
10
|
mkdirSync(HANDOFFS_DIR, { recursive: true });
|
|
@@ -66,26 +66,19 @@ export function registerSessionTools(server) {
|
|
|
66
66
|
}, async ({ agent_slug, limit }) => {
|
|
67
67
|
const maxItems = Math.min(limit ?? 10, 30);
|
|
68
68
|
const items = [];
|
|
69
|
-
// 1. Stale goals
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const urgency = Math.min(5, Math.floor(daysSinceUpdate / staleThreshold) + (goal.priority === 'high' ? 2 : goal.priority === 'medium' ? 1 : 0));
|
|
83
|
-
items.push({ type: 'stale-goal', urgency, description: `Goal "${goal.title}" stale for ${daysSinceUpdate}d (${goal.priority} priority)` });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
69
|
+
// 1. Stale goals (walks global + per-agent dirs)
|
|
70
|
+
for (const { goal, owner } of listAllGoals()) {
|
|
71
|
+
if (goal.status !== 'active')
|
|
72
|
+
continue;
|
|
73
|
+
if (agent_slug && owner !== agent_slug)
|
|
74
|
+
continue;
|
|
75
|
+
if (!goal.updatedAt)
|
|
76
|
+
continue;
|
|
77
|
+
const daysSinceUpdate = Math.floor((Date.now() - new Date(goal.updatedAt).getTime()) / 86400000);
|
|
78
|
+
const staleThreshold = goal.reviewFrequency === 'daily' ? 1 : goal.reviewFrequency === 'weekly' ? 7 : 30;
|
|
79
|
+
if (daysSinceUpdate > staleThreshold) {
|
|
80
|
+
const urgency = Math.min(5, Math.floor(daysSinceUpdate / staleThreshold) + (goal.priority === 'high' ? 2 : goal.priority === 'medium' ? 1 : 0));
|
|
81
|
+
items.push({ type: 'stale-goal', urgency, description: `Goal "${goal.title}" stale for ${daysSinceUpdate}d (${goal.priority} priority)` });
|
|
89
82
|
}
|
|
90
83
|
}
|
|
91
84
|
// 2. Failing cron jobs
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -169,6 +169,46 @@ export declare function agentTasksFile(slug: string | null): string;
|
|
|
169
169
|
export declare function agentWorkingMemoryFile(slug: string | null): string;
|
|
170
170
|
export declare function agentGoalsDir(slug: string | null): string;
|
|
171
171
|
export declare function agentDailyNotesDir(slug: string | null): string;
|
|
172
|
+
export type GoalRecord = {
|
|
173
|
+
id: string;
|
|
174
|
+
title: string;
|
|
175
|
+
description?: string;
|
|
176
|
+
owner: string;
|
|
177
|
+
status?: string;
|
|
178
|
+
priority?: string;
|
|
179
|
+
createdAt?: string;
|
|
180
|
+
updatedAt?: string;
|
|
181
|
+
progressNotes?: string[];
|
|
182
|
+
nextActions?: string[];
|
|
183
|
+
blockers?: string[];
|
|
184
|
+
reviewFrequency?: string;
|
|
185
|
+
linkedCronJobs?: string[];
|
|
186
|
+
[key: string]: unknown;
|
|
187
|
+
};
|
|
188
|
+
/** Return the directory where a goal owned by `owner` should live. */
|
|
189
|
+
export declare function goalDirForOwner(owner: string): string;
|
|
190
|
+
/**
|
|
191
|
+
* Walk Clementine's global goals dir AND every per-agent goals dir.
|
|
192
|
+
* Returns {goal, path, owner} for each goal found. Owner is derived from
|
|
193
|
+
* the goal's `owner` field if set, else inferred from which directory it was in.
|
|
194
|
+
*/
|
|
195
|
+
export declare function listAllGoals(): Array<{
|
|
196
|
+
goal: GoalRecord;
|
|
197
|
+
filePath: string;
|
|
198
|
+
owner: string;
|
|
199
|
+
}>;
|
|
200
|
+
/** Find a goal's file path by id across global + all agent dirs. */
|
|
201
|
+
export declare function findGoalPath(id: string): {
|
|
202
|
+
filePath: string;
|
|
203
|
+
owner: string;
|
|
204
|
+
} | null;
|
|
205
|
+
/** Read a goal by id; returns null if not found. */
|
|
206
|
+
export declare function readGoalById(id: string): GoalRecord | null;
|
|
207
|
+
/**
|
|
208
|
+
* Write a goal to the correct directory based on its owner field.
|
|
209
|
+
* Creates the target directory if needed. Returns the path written.
|
|
210
|
+
*/
|
|
211
|
+
export declare function writeGoalForOwner(goal: GoalRecord): string;
|
|
172
212
|
export declare function todayStr(): string;
|
|
173
213
|
export declare function yesterdayStr(): string;
|
|
174
214
|
export declare function nowTime(): string;
|
package/dist/tools/shared.js
CHANGED
|
@@ -96,6 +96,90 @@ export function agentDailyNotesDir(slug) {
|
|
|
96
96
|
return DAILY_NOTES_DIR;
|
|
97
97
|
return path.join(AGENTS_DIR, slug, 'daily-notes');
|
|
98
98
|
}
|
|
99
|
+
/** Return the directory where a goal owned by `owner` should live. */
|
|
100
|
+
export function goalDirForOwner(owner) {
|
|
101
|
+
if (!owner || owner === 'clementine')
|
|
102
|
+
return GOALS_DIR;
|
|
103
|
+
return path.join(AGENTS_DIR, owner, 'goals');
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Walk Clementine's global goals dir AND every per-agent goals dir.
|
|
107
|
+
* Returns {goal, path, owner} for each goal found. Owner is derived from
|
|
108
|
+
* the goal's `owner` field if set, else inferred from which directory it was in.
|
|
109
|
+
*/
|
|
110
|
+
export function listAllGoals() {
|
|
111
|
+
const results = [];
|
|
112
|
+
const readDir = (dir, inferredOwner) => {
|
|
113
|
+
if (!existsSync(dir))
|
|
114
|
+
return;
|
|
115
|
+
let files;
|
|
116
|
+
try {
|
|
117
|
+
files = readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
for (const f of files) {
|
|
123
|
+
const fp = path.join(dir, f);
|
|
124
|
+
try {
|
|
125
|
+
const goal = JSON.parse(readFileSync(fp, 'utf-8'));
|
|
126
|
+
if (!goal.id)
|
|
127
|
+
continue;
|
|
128
|
+
const owner = (typeof goal.owner === 'string' && goal.owner) || inferredOwner;
|
|
129
|
+
results.push({ goal, filePath: fp, owner });
|
|
130
|
+
}
|
|
131
|
+
catch { /* skip malformed */ }
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
readDir(GOALS_DIR, 'clementine');
|
|
135
|
+
if (existsSync(AGENTS_DIR)) {
|
|
136
|
+
let agentDirs = [];
|
|
137
|
+
try {
|
|
138
|
+
agentDirs = readdirSync(AGENTS_DIR, { withFileTypes: true })
|
|
139
|
+
.filter(d => d.isDirectory() && !d.name.startsWith('_'))
|
|
140
|
+
.map(d => d.name);
|
|
141
|
+
}
|
|
142
|
+
catch { /* ignore */ }
|
|
143
|
+
for (const slug of agentDirs) {
|
|
144
|
+
readDir(path.join(AGENTS_DIR, slug, 'goals'), slug);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return results;
|
|
148
|
+
}
|
|
149
|
+
/** Find a goal's file path by id across global + all agent dirs. */
|
|
150
|
+
export function findGoalPath(id) {
|
|
151
|
+
for (const entry of listAllGoals()) {
|
|
152
|
+
if (entry.goal.id === id) {
|
|
153
|
+
return { filePath: entry.filePath, owner: entry.owner };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
/** Read a goal by id; returns null if not found. */
|
|
159
|
+
export function readGoalById(id) {
|
|
160
|
+
const found = findGoalPath(id);
|
|
161
|
+
if (!found)
|
|
162
|
+
return null;
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(readFileSync(found.filePath, 'utf-8'));
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Write a goal to the correct directory based on its owner field.
|
|
172
|
+
* Creates the target directory if needed. Returns the path written.
|
|
173
|
+
*/
|
|
174
|
+
export function writeGoalForOwner(goal) {
|
|
175
|
+
const owner = goal.owner || 'clementine';
|
|
176
|
+
const dir = goalDirForOwner(owner);
|
|
177
|
+
if (!existsSync(dir))
|
|
178
|
+
mkdirSync(dir, { recursive: true });
|
|
179
|
+
const fp = path.join(dir, `${goal.id}.json`);
|
|
180
|
+
writeFileSync(fp, JSON.stringify(goal, null, 2));
|
|
181
|
+
return fp;
|
|
182
|
+
}
|
|
99
183
|
// ── Date/Time helpers ───────────────────────────────���──────────────────
|
|
100
184
|
export function todayStr() {
|
|
101
185
|
const d = new Date();
|