clementine-agent 1.0.92 → 1.0.94
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/execution-advisor.d.ts +17 -0
- package/dist/agent/execution-advisor.js +11 -3
- package/dist/agent/safe-restart.js +10 -1
- package/dist/agent/self-improve-loop.d.ts +16 -1
- package/dist/agent/self-improve-loop.js +61 -6
- package/dist/agent/self-improve.js +4 -2
- package/dist/channels/webhook.js +12 -5
- package/dist/config.d.ts +2 -0
- package/dist/config.js +12 -0
- package/dist/tools/admin-tools.js +16 -4
- package/package.json +1 -1
|
@@ -5,6 +5,23 @@
|
|
|
5
5
|
* cron job execution parameters: turn limits, models, timeouts, prompt
|
|
6
6
|
* enrichment, escalation, and circuit-breaking.
|
|
7
7
|
*/
|
|
8
|
+
import { CronRunLog } from '../gateway/heartbeat.js';
|
|
8
9
|
import type { CronJobDefinition, ExecutionAdvice } from '../types.js';
|
|
10
|
+
interface ReflectionEntry {
|
|
11
|
+
jobName: string;
|
|
12
|
+
timestamp: string;
|
|
13
|
+
existence: boolean;
|
|
14
|
+
substance: boolean;
|
|
15
|
+
actionable: boolean;
|
|
16
|
+
communication: boolean;
|
|
17
|
+
criteriaMet: boolean | null;
|
|
18
|
+
quality: number;
|
|
19
|
+
gap: string;
|
|
20
|
+
commNote: string;
|
|
21
|
+
}
|
|
9
22
|
export declare function getExecutionAdvice(jobName: string, job: CronJobDefinition): ExecutionAdvice;
|
|
23
|
+
export declare function checkTurnLimitHits(runs: ReturnType<CronRunLog['readRecent']>, job: CronJobDefinition, advice: ExecutionAdvice): void;
|
|
24
|
+
export declare function checkReflectionQuality(reflections: ReflectionEntry[], job: CronJobDefinition, advice: ExecutionAdvice): void;
|
|
25
|
+
export declare function checkTimeoutHits(runs: ReturnType<CronRunLog['readRecent']>, job: CronJobDefinition, advice: ExecutionAdvice): void;
|
|
26
|
+
export {};
|
|
10
27
|
//# sourceMappingURL=execution-advisor.d.ts.map
|
|
@@ -102,7 +102,11 @@ export function getExecutionAdvice(jobName, job) {
|
|
|
102
102
|
return advice;
|
|
103
103
|
}
|
|
104
104
|
// ── Rule helpers ────────────────────────────────────────────────────
|
|
105
|
-
function checkTurnLimitHits(runs, job, advice) {
|
|
105
|
+
export function checkTurnLimitHits(runs, job, advice) {
|
|
106
|
+
// Unleashed jobs manage per-phase turns via UNLEASHED_PHASE_TURNS, not job.maxTurns.
|
|
107
|
+
// Bumping maxTurns here would override the unleashed limit with a small value.
|
|
108
|
+
if (job.mode === 'unleashed')
|
|
109
|
+
return;
|
|
106
110
|
// Use precise TerminalReason when available, fall back to regex on error text
|
|
107
111
|
const turnLimitHits = runs.slice(0, 5).filter(r => {
|
|
108
112
|
if (r.status !== 'error' && r.status !== 'retried')
|
|
@@ -131,7 +135,9 @@ function checkTurnLimitHits(runs, job, advice) {
|
|
|
131
135
|
logger.debug({ job: job.name, from: currentMax, to: advice.adjustedMaxTurns }, 'Adjusting maxTurns due to turn-limit hits');
|
|
132
136
|
}
|
|
133
137
|
}
|
|
134
|
-
function checkReflectionQuality(reflections, job, advice) {
|
|
138
|
+
export function checkReflectionQuality(reflections, job, advice) {
|
|
139
|
+
if (job.mode === 'unleashed')
|
|
140
|
+
return;
|
|
135
141
|
const recent = reflections.slice(0, 5); // already newest-first
|
|
136
142
|
if (recent.length < 3)
|
|
137
143
|
return;
|
|
@@ -166,7 +172,9 @@ function checkModelUpgrade(runs, job, advice) {
|
|
|
166
172
|
logger.debug({ job: job.name, failures: recentFailures.length }, 'Upgrading model from haiku to sonnet due to repeated failures');
|
|
167
173
|
}
|
|
168
174
|
}
|
|
169
|
-
function checkTimeoutHits(runs, job, advice) {
|
|
175
|
+
export function checkTimeoutHits(runs, job, advice) {
|
|
176
|
+
if (job.mode === 'unleashed')
|
|
177
|
+
return;
|
|
170
178
|
const timeoutMs = DEFAULT_TIMEOUT_MS; // standard cron timeout
|
|
171
179
|
const threshold = timeoutMs * 0.95;
|
|
172
180
|
const timeoutHits = runs.slice(0, 5).filter(r => {
|
|
@@ -14,11 +14,15 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
14
14
|
import { randomBytes } from 'node:crypto';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import pino from 'pino';
|
|
17
|
-
import { BASE_DIR } from '../config.js';
|
|
17
|
+
import { ALLOW_SOURCE_EDITS, BASE_DIR } from '../config.js';
|
|
18
18
|
import { preflightSourceChange } from './source-preflight.js';
|
|
19
19
|
import { recordSourceMod } from './source-mods.js';
|
|
20
20
|
const logger = pino({ name: 'clementine.safe-restart' });
|
|
21
21
|
const SENTINEL_PATH = path.join(BASE_DIR, '.restart-sentinel.json');
|
|
22
|
+
const DEPRECATION_MESSAGE = 'Source self-editing is disabled. Use advisor-rule YAML, CRON.md frontmatter, ' +
|
|
23
|
+
'or prompt-override markdown instead — those land without a restart and survive ' +
|
|
24
|
+
'npm updates. Set CLEMENTINE_ALLOW_SOURCE_EDITS=1 in ~/.clementine/.env only for ' +
|
|
25
|
+
'genuine engine bugs that cannot be expressed as data.';
|
|
22
26
|
/** Files that cannot be self-edited (security-critical or self-referential). */
|
|
23
27
|
const BLOCKLIST = new Set([
|
|
24
28
|
'src/config.ts',
|
|
@@ -47,6 +51,11 @@ const BLOCKLIST = new Set([
|
|
|
47
51
|
*/
|
|
48
52
|
export async function safeSourceEdit(pkgDir, changes, opts) {
|
|
49
53
|
const reason = opts?.reason ?? 'source self-edit';
|
|
54
|
+
// Quarantine: source self-edit is deprecated. Off by default.
|
|
55
|
+
if (!ALLOW_SOURCE_EDITS) {
|
|
56
|
+
logger.warn({ fileCount: changes.length, files: changes.map(c => c.relativePath), reason }, 'Source self-edit refused — primitive is disabled');
|
|
57
|
+
return { success: false, error: DEPRECATION_MESSAGE };
|
|
58
|
+
}
|
|
50
59
|
// Validate against blocklist
|
|
51
60
|
for (const change of changes) {
|
|
52
61
|
if (BLOCKLIST.has(change.relativePath)) {
|
|
@@ -51,12 +51,21 @@ export interface SelfImproveDispatcher {
|
|
|
51
51
|
}>;
|
|
52
52
|
}
|
|
53
53
|
export interface SelfImproveLoopOptions {
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Override the fallback safety-net tick interval. The loop is primarily
|
|
56
|
+
* event-driven; this is just a backstop. Used by tests to disable the
|
|
57
|
+
* timer entirely (set to 0 or a very large number).
|
|
58
|
+
*/
|
|
55
59
|
tickMs?: number;
|
|
56
60
|
/** Override directories for tests. */
|
|
57
61
|
triggersDir?: string;
|
|
58
62
|
pendingDir?: string;
|
|
59
63
|
cronPath?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Disable the fs.watch event-driven path. Tests use this so they can
|
|
66
|
+
* call tick() directly without racing the watcher.
|
|
67
|
+
*/
|
|
68
|
+
disableWatch?: boolean;
|
|
60
69
|
}
|
|
61
70
|
export declare function classifyFailure(recentErrors: string[]): FixRecipe;
|
|
62
71
|
export declare class SelfImproveLoop {
|
|
@@ -65,12 +74,18 @@ export declare class SelfImproveLoop {
|
|
|
65
74
|
private readonly pendingDir;
|
|
66
75
|
private readonly cronPath;
|
|
67
76
|
private readonly dispatcher;
|
|
77
|
+
private readonly watchEnabled;
|
|
68
78
|
private timer;
|
|
79
|
+
private watcher;
|
|
80
|
+
private debounceTimer;
|
|
69
81
|
private running;
|
|
70
82
|
private ticking;
|
|
71
83
|
constructor(dispatcher: SelfImproveDispatcher, opts?: SelfImproveLoopOptions);
|
|
72
84
|
start(): void;
|
|
73
85
|
stop(): void;
|
|
86
|
+
/** Coalesce a burst of fs.watch events (multiple triggers landing in
|
|
87
|
+
* quick succession) into a single tick. */
|
|
88
|
+
private scheduleDebouncedTick;
|
|
74
89
|
/**
|
|
75
90
|
* Process all pending triggers. Public so tests + manual invocations
|
|
76
91
|
* (e.g., a `clementine self-improve tick` CLI command) can call it.
|
|
@@ -24,13 +24,25 @@
|
|
|
24
24
|
* Idempotent: re-applying the same fix to an already-fixed job is a
|
|
25
25
|
* no-op; the trigger gets removed regardless.
|
|
26
26
|
*/
|
|
27
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
27
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, watch, writeFileSync, } from 'node:fs';
|
|
28
28
|
import path from 'node:path';
|
|
29
29
|
import matter from 'gray-matter';
|
|
30
30
|
import pino from 'pino';
|
|
31
31
|
import { BASE_DIR, SYSTEM_DIR } from '../config.js';
|
|
32
32
|
const logger = pino({ name: 'clementine.self-improve-loop' });
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Fallback tick interval. The loop is primarily event-driven via fs.watch
|
|
35
|
+
* on the triggers directory — this is just a slow safety net for cases
|
|
36
|
+
* where fs.watch dropped an event (rare but possible) or where the
|
|
37
|
+
* daemon booted with triggers already in place from before fs.watch was
|
|
38
|
+
* registered. 1h is plenty: the upstream cron scheduler runs at most
|
|
39
|
+
* once per minute, and a job needs 3+ consecutive errors to even produce
|
|
40
|
+
* a trigger, so the situation is already hours-stale by the time we see
|
|
41
|
+
* a trigger.
|
|
42
|
+
*/
|
|
43
|
+
const FALLBACK_TICK_MS = 60 * 60 * 1000;
|
|
44
|
+
/** Coalesce a burst of fs.watch events into a single tick. */
|
|
45
|
+
const WATCH_DEBOUNCE_MS = 2000;
|
|
34
46
|
const TRIGGERS_DIR = path.join(BASE_DIR, 'self-improve', 'triggers');
|
|
35
47
|
const PENDING_CHANGES_DIR = path.join(BASE_DIR, 'self-improve', 'pending-changes');
|
|
36
48
|
const CRON_PATH = path.join(SYSTEM_DIR, 'CRON.md');
|
|
@@ -141,27 +153,49 @@ export class SelfImproveLoop {
|
|
|
141
153
|
pendingDir;
|
|
142
154
|
cronPath;
|
|
143
155
|
dispatcher;
|
|
156
|
+
watchEnabled;
|
|
144
157
|
timer = null;
|
|
158
|
+
watcher = null;
|
|
159
|
+
debounceTimer = null;
|
|
145
160
|
running = false;
|
|
146
161
|
ticking = false;
|
|
147
162
|
constructor(dispatcher, opts = {}) {
|
|
148
163
|
this.dispatcher = dispatcher;
|
|
149
|
-
this.tickMs = opts.tickMs ??
|
|
164
|
+
this.tickMs = opts.tickMs ?? FALLBACK_TICK_MS;
|
|
150
165
|
this.triggersDir = opts.triggersDir ?? TRIGGERS_DIR;
|
|
151
166
|
this.pendingDir = opts.pendingDir ?? PENDING_CHANGES_DIR;
|
|
152
167
|
this.cronPath = opts.cronPath ?? CRON_PATH;
|
|
168
|
+
this.watchEnabled = opts.disableWatch !== true;
|
|
153
169
|
}
|
|
154
170
|
start() {
|
|
155
171
|
if (this.running)
|
|
156
172
|
return;
|
|
157
173
|
this.running = true;
|
|
158
174
|
// Run immediately so any backlog from the prior daemon gets handled
|
|
159
|
-
// without a
|
|
175
|
+
// without a long wait.
|
|
160
176
|
this.tick().catch((err) => logger.error({ err }, 'Initial self-improve tick failed'));
|
|
177
|
+
// Event-driven primary path: watch the triggers dir. cron-scheduler
|
|
178
|
+
// writes a file when a job hits consErrors >= 3; we react within
|
|
179
|
+
// ~2 seconds (debounce window) instead of polling every 10 minutes
|
|
180
|
+
// for a directory that's empty 99% of the time.
|
|
181
|
+
if (this.watchEnabled) {
|
|
182
|
+
try {
|
|
183
|
+
mkdirSync(this.triggersDir, { recursive: true });
|
|
184
|
+
this.watcher = watch(this.triggersDir, (eventType, filename) => {
|
|
185
|
+
if (eventType !== 'rename' || !filename || !filename.endsWith('.json'))
|
|
186
|
+
return;
|
|
187
|
+
this.scheduleDebouncedTick();
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
logger.warn({ err, dir: this.triggersDir }, 'Failed to watch triggers dir — falling back to polling only');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Slow fallback safety net — covers fs.watch event drops + boot-with-backlog.
|
|
161
195
|
this.timer = setInterval(() => {
|
|
162
|
-
this.tick().catch((err) => logger.error({ err }, 'Self-improve tick failed'));
|
|
196
|
+
this.tick().catch((err) => logger.error({ err }, 'Self-improve fallback tick failed'));
|
|
163
197
|
}, this.tickMs);
|
|
164
|
-
logger.info({
|
|
198
|
+
logger.info({ fallbackTickMs: this.tickMs, watchEnabled: this.watchEnabled && this.watcher !== null }, 'Self-improve loop started');
|
|
165
199
|
}
|
|
166
200
|
stop() {
|
|
167
201
|
if (!this.running)
|
|
@@ -171,8 +205,29 @@ export class SelfImproveLoop {
|
|
|
171
205
|
clearInterval(this.timer);
|
|
172
206
|
this.timer = null;
|
|
173
207
|
}
|
|
208
|
+
if (this.watcher) {
|
|
209
|
+
try {
|
|
210
|
+
this.watcher.close();
|
|
211
|
+
}
|
|
212
|
+
catch { /* ignore */ }
|
|
213
|
+
this.watcher = null;
|
|
214
|
+
}
|
|
215
|
+
if (this.debounceTimer) {
|
|
216
|
+
clearTimeout(this.debounceTimer);
|
|
217
|
+
this.debounceTimer = null;
|
|
218
|
+
}
|
|
174
219
|
logger.info('Self-improve loop stopped');
|
|
175
220
|
}
|
|
221
|
+
/** Coalesce a burst of fs.watch events (multiple triggers landing in
|
|
222
|
+
* quick succession) into a single tick. */
|
|
223
|
+
scheduleDebouncedTick() {
|
|
224
|
+
if (this.debounceTimer)
|
|
225
|
+
clearTimeout(this.debounceTimer);
|
|
226
|
+
this.debounceTimer = setTimeout(() => {
|
|
227
|
+
this.debounceTimer = null;
|
|
228
|
+
this.tick().catch((err) => logger.error({ err }, 'Self-improve event-driven tick failed'));
|
|
229
|
+
}, WATCH_DEBOUNCE_MS);
|
|
230
|
+
}
|
|
176
231
|
/**
|
|
177
232
|
* Process all pending triggers. Public so tests + manual invocations
|
|
178
233
|
* (e.g., a `clementine self-improve tick` CLI command) can call it.
|
|
@@ -23,9 +23,11 @@ const DEFAULT_CONFIG = {
|
|
|
23
23
|
maxDurationMs: 3_600_000, // 1 hour
|
|
24
24
|
acceptThreshold: 0.7,
|
|
25
25
|
plateauLimit: 3,
|
|
26
|
-
|
|
26
|
+
// 'source' deprecated — self-improvement should produce data (cron, workflow, etc.),
|
|
27
|
+
// not engine TS edits. Re-add only with CLEMENTINE_ALLOW_SOURCE_EDITS=1.
|
|
28
|
+
areas: ['soul', 'cron', 'workflow', 'memory', 'agent', 'communication', 'goal'],
|
|
27
29
|
autoApply: true,
|
|
28
|
-
sourceMode: '
|
|
30
|
+
sourceMode: 'skip',
|
|
29
31
|
};
|
|
30
32
|
// ── Paths ────────────────────────────────────────────────────────────
|
|
31
33
|
const EXPERIMENT_LOG = path.join(SELF_IMPROVE_DIR, 'experiment-log.jsonl');
|
package/dist/channels/webhook.js
CHANGED
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import express from 'express';
|
|
8
8
|
import pino from 'pino';
|
|
9
|
-
import { WEBHOOK_PORT, WEBHOOK_SECRET } from '../config.js';
|
|
9
|
+
import { WEBHOOK_BIND, WEBHOOK_PORT, WEBHOOK_SECRET } from '../config.js';
|
|
10
10
|
const logger = pino({ name: 'clementine.webhook' });
|
|
11
11
|
// ── Entry point ───────────────────────────────────────────────────────
|
|
12
12
|
export async function startWebhook(gateway) {
|
|
13
|
+
if (!WEBHOOK_SECRET) {
|
|
14
|
+
throw new Error('WEBHOOK_ENABLED=true requires WEBHOOK_SECRET to be set. Refusing to start an unauthenticated webhook server.');
|
|
15
|
+
}
|
|
13
16
|
const app = express();
|
|
14
17
|
app.use(express.json());
|
|
15
18
|
// ── Bearer token auth middleware ──────────────────────────────────
|
|
@@ -54,8 +57,8 @@ export async function startWebhook(gateway) {
|
|
|
54
57
|
res.status(500).json({ error: 'Internal server error' });
|
|
55
58
|
}
|
|
56
59
|
});
|
|
57
|
-
// ── GET /api/status — health check
|
|
58
|
-
app.get('/api/status', (_req, res) => {
|
|
60
|
+
// ── GET /api/status — health check (auth-gated to avoid uptime leakage) ──
|
|
61
|
+
app.get('/api/status', requireAuth, (_req, res) => {
|
|
59
62
|
res.json({
|
|
60
63
|
status: 'ok',
|
|
61
64
|
uptime: process.uptime(),
|
|
@@ -64,9 +67,13 @@ export async function startWebhook(gateway) {
|
|
|
64
67
|
});
|
|
65
68
|
// ── Start server ──────────────────────────────────────────────────
|
|
66
69
|
const port = WEBHOOK_PORT;
|
|
70
|
+
const bind = WEBHOOK_BIND;
|
|
71
|
+
if (bind !== '127.0.0.1' && bind !== 'localhost') {
|
|
72
|
+
logger.warn({ bind }, '⚠ Webhook bound to non-localhost address — bearer auth is your only protection. Prefer tunneling (cloudflared) over exposing directly.');
|
|
73
|
+
}
|
|
67
74
|
await new Promise((resolve) => {
|
|
68
|
-
app.listen(port,
|
|
69
|
-
logger.info(
|
|
75
|
+
app.listen(port, bind, () => {
|
|
76
|
+
logger.info({ bind, port }, 'Webhook API server listening');
|
|
70
77
|
resolve();
|
|
71
78
|
});
|
|
72
79
|
});
|
package/dist/config.d.ts
CHANGED
|
@@ -80,6 +80,7 @@ export declare const WHATSAPP_WEBHOOK_PORT: number;
|
|
|
80
80
|
export declare const WEBHOOK_ENABLED: boolean;
|
|
81
81
|
export declare const WEBHOOK_PORT: number;
|
|
82
82
|
export declare const WEBHOOK_SECRET: string;
|
|
83
|
+
export declare const WEBHOOK_BIND: string;
|
|
83
84
|
export declare const GROQ_API_KEY: string;
|
|
84
85
|
export declare const ELEVENLABS_API_KEY: string;
|
|
85
86
|
export declare const ELEVENLABS_VOICE_ID: string;
|
|
@@ -150,6 +151,7 @@ export declare const PLANS_DIR: string;
|
|
|
150
151
|
export declare const ADVISOR_LOG_PATH: string;
|
|
151
152
|
export declare const REMOTE_ACCESS_CONFIG: string;
|
|
152
153
|
export declare const STAGING_DIR: string;
|
|
154
|
+
export declare const ALLOW_SOURCE_EDITS: boolean;
|
|
153
155
|
export declare const CLAUDE_CODE_OAUTH_TOKEN: string;
|
|
154
156
|
export declare const ANTHROPIC_API_KEY: string;
|
|
155
157
|
export declare const CREDENTIALS_FILE: string;
|
package/dist/config.js
CHANGED
|
@@ -172,6 +172,9 @@ export const WHATSAPP_WEBHOOK_PORT = parseInt(getEnv('WHATSAPP_WEBHOOK_PORT', '8
|
|
|
172
172
|
export const WEBHOOK_ENABLED = getEnv('WEBHOOK_ENABLED', 'false').toLowerCase() === 'true';
|
|
173
173
|
export const WEBHOOK_PORT = parseInt(getEnv('WEBHOOK_PORT', '8420'), 10);
|
|
174
174
|
export const WEBHOOK_SECRET = getSecret('WEBHOOK_SECRET');
|
|
175
|
+
// Default bind to localhost only — flip to 0.0.0.0 explicitly via WEBHOOK_BIND
|
|
176
|
+
// for tunneled or LAN-exposed setups. Avoids the OpenClaw CVE-2026-25253 shape.
|
|
177
|
+
export const WEBHOOK_BIND = getEnv('WEBHOOK_BIND', '127.0.0.1');
|
|
175
178
|
// ── Voice ────────────────────────────────────────────────────────────
|
|
176
179
|
export const GROQ_API_KEY = getSecret('GROQ_API_KEY');
|
|
177
180
|
export const ELEVENLABS_API_KEY = getSecret('ELEVENLABS_API_KEY');
|
|
@@ -302,6 +305,15 @@ export const ADVISOR_LOG_PATH = path.join(BASE_DIR, 'cron', 'advisor-decisions.j
|
|
|
302
305
|
export const REMOTE_ACCESS_CONFIG = path.join(BASE_DIR, 'remote-access.json');
|
|
303
306
|
// ── Source Self-Edit Staging ─────────────────────────────────────────
|
|
304
307
|
export const STAGING_DIR = path.join(BASE_DIR, 'staging');
|
|
308
|
+
// Source self-editing is deprecated. The data-driven path (advisor rules,
|
|
309
|
+
// CRON.md frontmatter, prompt overrides) is the supported way to evolve
|
|
310
|
+
// behavior without requiring a new release. Set CLEMENTINE_ALLOW_SOURCE_EDITS=1
|
|
311
|
+
// in ~/.clementine/.env to re-enable for genuine engine bugs that can't be
|
|
312
|
+
// expressed as data — the primitive itself stays on disk for that escape hatch.
|
|
313
|
+
export const ALLOW_SOURCE_EDITS = (() => {
|
|
314
|
+
const raw = getEnv('CLEMENTINE_ALLOW_SOURCE_EDITS', '').toLowerCase().trim();
|
|
315
|
+
return raw === '1' || raw === 'true' || raw === 'yes';
|
|
316
|
+
})();
|
|
305
317
|
// ── API ──────────────────────────────────────────────────────────────
|
|
306
318
|
// Long-lived OAuth token from `clementine login` / `claude setup-token`.
|
|
307
319
|
// Takes priority over ANTHROPIC_API_KEY in the SDK subprocess env.
|
|
@@ -15,6 +15,7 @@ import path from 'node:path';
|
|
|
15
15
|
import Anthropic from '@anthropic-ai/sdk';
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { BASE_DIR, CRON_FILE, SYSTEM_DIR, env, getStore, logger, textResult, } from './shared.js';
|
|
18
|
+
import { ALLOW_SOURCE_EDITS } from '../config.js';
|
|
18
19
|
import { getInteractionSource } from '../agent/hooks.js';
|
|
19
20
|
import { renameSync } from 'node:fs';
|
|
20
21
|
import * as keychain from '../secrets/keychain.js';
|
|
@@ -1628,11 +1629,22 @@ export function registerAdminTools(server) {
|
|
|
1628
1629
|
// ── Source Self-Edit Tools ──────────────────────────────────────────────
|
|
1629
1630
|
const SELF_IMPROVE_DIR = path.join(BASE_DIR, 'self-improve');
|
|
1630
1631
|
const PENDING_SOURCE_DIR = path.join(SELF_IMPROVE_DIR, 'pending-source-changes');
|
|
1631
|
-
server.tool('self_edit_source', '
|
|
1632
|
-
file: z.string().describe('Path relative to src/
|
|
1632
|
+
server.tool('self_edit_source', 'DEPRECATED — source self-editing is disabled. Improvements should be expressed as data, not TypeScript: edit advisor rules in ~/.clementine/advisor-rules/, CRON.md frontmatter, or prompt overrides. Those land without a restart and survive npm updates. Only re-enabled (CLEMENTINE_ALLOW_SOURCE_EDITS=1) for genuine engine bugs that cannot be expressed as data.', {
|
|
1633
|
+
file: z.string().describe('Path relative to src/'),
|
|
1633
1634
|
content: z.string().describe('Complete new file content'),
|
|
1634
1635
|
reason: z.string().describe('Why this change is being made'),
|
|
1635
|
-
}, async ({ file, content, reason }) => {
|
|
1636
|
+
}, async ({ file, content: _content, reason }) => {
|
|
1637
|
+
if (!ALLOW_SOURCE_EDITS) {
|
|
1638
|
+
logger.warn({ file, reason }, 'self_edit_source called while disabled — rejecting');
|
|
1639
|
+
return textResult('Source self-editing is disabled. The fix you want belongs in user-space data, not engine TypeScript:\n' +
|
|
1640
|
+
' • Cron job behavior → edit ~/.clementine/vault/00-System/CRON.md frontmatter\n' +
|
|
1641
|
+
' • Advisor logic → write a YAML file in ~/.clementine/advisor-rules/ (coming in next phase)\n' +
|
|
1642
|
+
' • Prompt content → edit the per-job prompt in CRON.md\n' +
|
|
1643
|
+
' • Agent behavior → edit the agent.md / SOUL.md\n\n' +
|
|
1644
|
+
'Those changes land without a restart and survive `npm update -g clementine`. ' +
|
|
1645
|
+
'If this is a genuine engine bug that cannot be expressed as data, ask the owner to set ' +
|
|
1646
|
+
'CLEMENTINE_ALLOW_SOURCE_EDITS=1 in ~/.clementine/.env.');
|
|
1647
|
+
}
|
|
1636
1648
|
// Security blocklist
|
|
1637
1649
|
const BLOCKLIST = ['config.ts', 'gateway/security-scanner.ts', 'security/scanner.ts'];
|
|
1638
1650
|
if (BLOCKLIST.some(b => file === b || file.startsWith(b))) {
|
|
@@ -1646,7 +1658,7 @@ export function registerAdminTools(server) {
|
|
|
1646
1658
|
const pending = {
|
|
1647
1659
|
id,
|
|
1648
1660
|
file: `src/${file}`,
|
|
1649
|
-
content,
|
|
1661
|
+
content: _content,
|
|
1650
1662
|
reason,
|
|
1651
1663
|
createdAt: new Date().toISOString(),
|
|
1652
1664
|
};
|