clementine-agent 1.0.95 → 1.0.97
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/prompt-overrides/loader.d.ts +27 -0
- package/dist/agent/prompt-overrides/loader.js +160 -0
- package/dist/agent/prompt-overrides/types.d.ts +34 -0
- package/dist/agent/prompt-overrides/types.js +17 -0
- package/dist/gateway/cron-scheduler.js +13 -0
- package/dist/gateway/failure-diagnostics.d.ts +26 -9
- package/dist/gateway/failure-diagnostics.js +89 -25
- package/dist/gateway/failure-monitor.d.ts +1 -22
- package/dist/gateway/fix-applier.d.ts +14 -10
- package/dist/gateway/fix-applier.js +124 -5
- package/dist/tools/admin-tools.js +66 -0
- package/package.json +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Overrides — loader.
|
|
3
|
+
*
|
|
4
|
+
* Reads markdown files from ~/.clementine/prompt-overrides/ and serves
|
|
5
|
+
* scope-resolved override text for a given (jobName, agentSlug). Hot-reloads
|
|
6
|
+
* via fs.watch (debounced).
|
|
7
|
+
*
|
|
8
|
+
* No package builtins — these are purely user/LLM authored. The directory
|
|
9
|
+
* is created on first load so users have an obvious empty home for overrides.
|
|
10
|
+
*/
|
|
11
|
+
import type { PromptOverride } from './types.js';
|
|
12
|
+
export interface LoaderOptions {
|
|
13
|
+
baseDir?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function loadPromptOverrides(opts?: LoaderOptions): PromptOverride[];
|
|
16
|
+
export declare function getLoadedOverrides(): PromptOverride[];
|
|
17
|
+
/**
|
|
18
|
+
* Resolve overrides applicable to a given job: global + agent (if agentSlug
|
|
19
|
+
* matches) + job (if jobName matches), sorted by priority ascending and
|
|
20
|
+
* concatenated into a single string. Empty if no overrides apply.
|
|
21
|
+
*/
|
|
22
|
+
export declare function loadPromptOverridesForJob(jobName: string, agentSlug?: string, opts?: LoaderOptions): string;
|
|
23
|
+
/** Install fs.watch on the overrides directory tree. Safe to call multiple times. */
|
|
24
|
+
export declare function watchPromptOverrides(opts?: LoaderOptions): void;
|
|
25
|
+
/** Test-only: clear cached state. */
|
|
26
|
+
export declare function _resetLoaderState(): void;
|
|
27
|
+
//# sourceMappingURL=loader.d.ts.map
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Overrides — loader.
|
|
3
|
+
*
|
|
4
|
+
* Reads markdown files from ~/.clementine/prompt-overrides/ and serves
|
|
5
|
+
* scope-resolved override text for a given (jobName, agentSlug). Hot-reloads
|
|
6
|
+
* via fs.watch (debounced).
|
|
7
|
+
*
|
|
8
|
+
* No package builtins — these are purely user/LLM authored. The directory
|
|
9
|
+
* is created on first load so users have an obvious empty home for overrides.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, watch as fsWatch } from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import pino from 'pino';
|
|
14
|
+
import matter from 'gray-matter';
|
|
15
|
+
import { BASE_DIR } from '../../config.js';
|
|
16
|
+
const logger = pino({ name: 'clementine.prompt-overrides' });
|
|
17
|
+
function rootDir(baseDir) {
|
|
18
|
+
return path.join(baseDir, 'prompt-overrides');
|
|
19
|
+
}
|
|
20
|
+
// ── State ────────────────────────────────────────────────────────────
|
|
21
|
+
let cached = [];
|
|
22
|
+
let watcherInstalled = false;
|
|
23
|
+
let watchDebounce = null;
|
|
24
|
+
// ── Parse one file ───────────────────────────────────────────────────
|
|
25
|
+
function parseOverride(filePath, scope, scopeKey) {
|
|
26
|
+
try {
|
|
27
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
28
|
+
const parsed = matter(raw);
|
|
29
|
+
const fm = (parsed.data ?? {});
|
|
30
|
+
const body = parsed.content.trim();
|
|
31
|
+
if (!body)
|
|
32
|
+
return null;
|
|
33
|
+
const defaultPriority = scope === 'global' ? 10 :
|
|
34
|
+
scope === 'agent' ? 50 :
|
|
35
|
+
100;
|
|
36
|
+
return {
|
|
37
|
+
body,
|
|
38
|
+
priority: typeof fm.priority === 'number' ? fm.priority : defaultPriority,
|
|
39
|
+
sourcePath: filePath,
|
|
40
|
+
scope,
|
|
41
|
+
scopeKey,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
logger.warn({ err, filePath }, 'Failed to parse prompt override — skipping');
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function readMarkdownFiles(dir) {
|
|
50
|
+
if (!existsSync(dir))
|
|
51
|
+
return [];
|
|
52
|
+
return readdirSync(dir)
|
|
53
|
+
.filter(f => f.endsWith('.md'))
|
|
54
|
+
.map(f => path.join(dir, f));
|
|
55
|
+
}
|
|
56
|
+
// ── Loader ───────────────────────────────────────────────────────────
|
|
57
|
+
export function loadPromptOverrides(opts) {
|
|
58
|
+
const baseDir = opts?.baseDir ?? BASE_DIR;
|
|
59
|
+
const root = rootDir(baseDir);
|
|
60
|
+
if (!existsSync(root)) {
|
|
61
|
+
mkdirSync(root, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
const out = [];
|
|
64
|
+
// _global.md (or any *.md) at root with bare name "_global"
|
|
65
|
+
const globalPath = path.join(root, '_global.md');
|
|
66
|
+
if (existsSync(globalPath)) {
|
|
67
|
+
const o = parseOverride(globalPath, 'global', null);
|
|
68
|
+
if (o)
|
|
69
|
+
out.push(o);
|
|
70
|
+
}
|
|
71
|
+
// agents/<slug>.md
|
|
72
|
+
for (const f of readMarkdownFiles(path.join(root, 'agents'))) {
|
|
73
|
+
const slug = path.basename(f, '.md');
|
|
74
|
+
const o = parseOverride(f, 'agent', slug);
|
|
75
|
+
if (o)
|
|
76
|
+
out.push(o);
|
|
77
|
+
}
|
|
78
|
+
// jobs/<jobName>.md
|
|
79
|
+
for (const f of readMarkdownFiles(path.join(root, 'jobs'))) {
|
|
80
|
+
const jobName = path.basename(f, '.md');
|
|
81
|
+
const o = parseOverride(f, 'job', jobName);
|
|
82
|
+
if (o)
|
|
83
|
+
out.push(o);
|
|
84
|
+
}
|
|
85
|
+
cached = out;
|
|
86
|
+
logger.info({ total: out.length, global: out.filter(o => o.scope === 'global').length, agent: out.filter(o => o.scope === 'agent').length, job: out.filter(o => o.scope === 'job').length }, 'Prompt overrides loaded');
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
export function getLoadedOverrides() {
|
|
90
|
+
return cached;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolve overrides applicable to a given job: global + agent (if agentSlug
|
|
94
|
+
* matches) + job (if jobName matches), sorted by priority ascending and
|
|
95
|
+
* concatenated into a single string. Empty if no overrides apply.
|
|
96
|
+
*/
|
|
97
|
+
export function loadPromptOverridesForJob(jobName, agentSlug, opts) {
|
|
98
|
+
// Use cached if loaded, else load fresh.
|
|
99
|
+
if (cached.length === 0) {
|
|
100
|
+
loadPromptOverrides(opts);
|
|
101
|
+
}
|
|
102
|
+
const applicable = cached.filter(o => {
|
|
103
|
+
if (o.scope === 'global')
|
|
104
|
+
return true;
|
|
105
|
+
if (o.scope === 'agent')
|
|
106
|
+
return agentSlug != null && o.scopeKey === agentSlug;
|
|
107
|
+
if (o.scope === 'job')
|
|
108
|
+
return o.scopeKey === jobName;
|
|
109
|
+
return false;
|
|
110
|
+
});
|
|
111
|
+
if (applicable.length === 0)
|
|
112
|
+
return '';
|
|
113
|
+
applicable.sort((a, b) => a.priority - b.priority);
|
|
114
|
+
return applicable.map(o => o.body).join('\n\n');
|
|
115
|
+
}
|
|
116
|
+
/** Install fs.watch on the overrides directory tree. Safe to call multiple times. */
|
|
117
|
+
export function watchPromptOverrides(opts) {
|
|
118
|
+
if (watcherInstalled)
|
|
119
|
+
return;
|
|
120
|
+
const baseDir = opts?.baseDir ?? BASE_DIR;
|
|
121
|
+
const root = rootDir(baseDir);
|
|
122
|
+
if (!existsSync(root)) {
|
|
123
|
+
mkdirSync(root, { recursive: true });
|
|
124
|
+
}
|
|
125
|
+
// Pre-create subdirs so fs.watch picks up future changes
|
|
126
|
+
for (const sub of ['agents', 'jobs']) {
|
|
127
|
+
const p = path.join(root, sub);
|
|
128
|
+
if (!existsSync(p))
|
|
129
|
+
mkdirSync(p, { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
fsWatch(root, { recursive: true }, () => {
|
|
133
|
+
if (watchDebounce)
|
|
134
|
+
clearTimeout(watchDebounce);
|
|
135
|
+
watchDebounce = setTimeout(() => {
|
|
136
|
+
try {
|
|
137
|
+
loadPromptOverrides(opts);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
logger.warn({ err }, 'Hot reload failed — keeping previous overrides');
|
|
141
|
+
}
|
|
142
|
+
}, 250);
|
|
143
|
+
});
|
|
144
|
+
watcherInstalled = true;
|
|
145
|
+
logger.debug({ root }, 'Watching prompt-overrides for hot reload');
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
logger.warn({ err }, 'Failed to install prompt-overrides watcher — hot reload disabled');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Test-only: clear cached state. */
|
|
152
|
+
export function _resetLoaderState() {
|
|
153
|
+
cached = [];
|
|
154
|
+
watcherInstalled = false;
|
|
155
|
+
if (watchDebounce) {
|
|
156
|
+
clearTimeout(watchDebounce);
|
|
157
|
+
watchDebounce = null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Overrides — schema types.
|
|
3
|
+
*
|
|
4
|
+
* Markdown files in ~/.clementine/prompt-overrides/ that get prepended to
|
|
5
|
+
* cron job prompts at runtime. Frontmatter is optional; the bare body of
|
|
6
|
+
* a markdown file is enough to override a prompt.
|
|
7
|
+
*
|
|
8
|
+
* Layout:
|
|
9
|
+
* _global.md — appended for every job (low priority)
|
|
10
|
+
* agents/<slug>.md — every job for an agent
|
|
11
|
+
* jobs/<jobName>.md — specific job
|
|
12
|
+
*
|
|
13
|
+
* Default priority: global=10, agent=50, job=100. Lower priority concatenates first
|
|
14
|
+
* (i.e. "outermost" — read by the LLM earlier).
|
|
15
|
+
*/
|
|
16
|
+
export type OverridePosition = 'append' | 'prepend';
|
|
17
|
+
export interface PromptOverrideFrontmatter {
|
|
18
|
+
schemaVersion?: 1;
|
|
19
|
+
priority?: number;
|
|
20
|
+
position?: OverridePosition;
|
|
21
|
+
}
|
|
22
|
+
export interface PromptOverride {
|
|
23
|
+
/** Raw body content (markdown), with frontmatter stripped. */
|
|
24
|
+
body: string;
|
|
25
|
+
/** Effective priority (frontmatter > scope default). */
|
|
26
|
+
priority: number;
|
|
27
|
+
/** Where the file lives — for logging only. */
|
|
28
|
+
sourcePath: string;
|
|
29
|
+
/** What scope this override targets. */
|
|
30
|
+
scope: 'global' | 'agent' | 'job';
|
|
31
|
+
/** For job/agent scope, the matched name; null for global. */
|
|
32
|
+
scopeKey: string | null;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Overrides — schema types.
|
|
3
|
+
*
|
|
4
|
+
* Markdown files in ~/.clementine/prompt-overrides/ that get prepended to
|
|
5
|
+
* cron job prompts at runtime. Frontmatter is optional; the bare body of
|
|
6
|
+
* a markdown file is enough to override a prompt.
|
|
7
|
+
*
|
|
8
|
+
* Layout:
|
|
9
|
+
* _global.md — appended for every job (low priority)
|
|
10
|
+
* agents/<slug>.md — every job for an agent
|
|
11
|
+
* jobs/<jobName>.md — specific job
|
|
12
|
+
*
|
|
13
|
+
* Default priority: global=10, agent=50, job=100. Lower priority concatenates first
|
|
14
|
+
* (i.e. "outermost" — read by the LLM earlier).
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -17,6 +17,7 @@ import { listAllGoals, findGoalPath, readGoalById } from '../tools/shared.js';
|
|
|
17
17
|
import { scanner } from '../security/scanner.js';
|
|
18
18
|
import { parseAllWorkflows as parseAllWorkflowsSync } from '../agent/workflow-runner.js';
|
|
19
19
|
import { SelfImproveLoop } from '../agent/self-improve.js';
|
|
20
|
+
import { loadPromptOverridesForJob, watchPromptOverrides } from '../agent/prompt-overrides/loader.js';
|
|
20
21
|
import { logAuditJsonl } from '../agent/hooks.js';
|
|
21
22
|
import { listBackgroundTasks, markDone as markBgTaskDone, markFailed as markBgTaskFailed, markRunning as markBgTaskRunning, } from '../agent/background-tasks.js';
|
|
22
23
|
import { outcomeStatusFromGoalDisposition, recentDecisions, recordDecisionOutcome, } from '../agent/proactive-ledger.js';
|
|
@@ -451,6 +452,7 @@ export class CronScheduler {
|
|
|
451
452
|
this.watchCronFile();
|
|
452
453
|
this.watchAgentsDir();
|
|
453
454
|
this.watchWorkflowDir();
|
|
455
|
+
watchPromptOverrides();
|
|
454
456
|
this.watchTriggers();
|
|
455
457
|
// Deep-mode jobs are owned by the router (_deliverDeepResult). The
|
|
456
458
|
// cron-scheduler callbacks below only dispatch for cron-originated runs;
|
|
@@ -888,6 +890,17 @@ export class CronScheduler {
|
|
|
888
890
|
...advisorApplied,
|
|
889
891
|
}, 'Execution advisor applied overrides');
|
|
890
892
|
}
|
|
893
|
+
// User-authored prompt overrides — applied AFTER advisor enrichment so user
|
|
894
|
+
// intent sits at the top of the final prompt. These are static per-file,
|
|
895
|
+
// not subject to advisor suppression rules.
|
|
896
|
+
const userOverride = loadPromptOverridesForJob(job.name, job.agentSlug);
|
|
897
|
+
if (userOverride) {
|
|
898
|
+
// Mutable copy if not already cloned by the advisor block above.
|
|
899
|
+
if (!advisorApplied)
|
|
900
|
+
job = { ...job };
|
|
901
|
+
job.prompt = `${userOverride}\n\n${job.prompt}`;
|
|
902
|
+
logger.debug({ job: job.name, overrideLen: userOverride.length }, 'Applied user prompt overrides');
|
|
903
|
+
}
|
|
891
904
|
// Compute effective timeout: advisor override > standard default
|
|
892
905
|
const effectiveTimeoutMs = job.mode !== 'unleashed'
|
|
893
906
|
? (advice.adjustedTimeoutMs ?? CRON_STANDARD_TIMEOUT_MS)
|
|
@@ -27,23 +27,40 @@ export type FixOperation = {
|
|
|
27
27
|
op: 'remove';
|
|
28
28
|
field: string;
|
|
29
29
|
};
|
|
30
|
+
/** CRON.md frontmatter edit (the original auto-apply shape). */
|
|
31
|
+
export interface AutoApplyCron {
|
|
32
|
+
kind?: 'cron';
|
|
33
|
+
agentSlug?: string;
|
|
34
|
+
operations: FixOperation[];
|
|
35
|
+
}
|
|
36
|
+
/** Write a YAML rule to ~/.clementine/advisor-rules/user/<ruleId>.yaml */
|
|
37
|
+
export interface AutoApplyAdvisorRule {
|
|
38
|
+
kind: 'advisor-rule';
|
|
39
|
+
ruleId: string;
|
|
40
|
+
yamlContent: string;
|
|
41
|
+
}
|
|
42
|
+
/** Write a markdown override to ~/.clementine/prompt-overrides/... */
|
|
43
|
+
export interface AutoApplyPromptOverride {
|
|
44
|
+
kind: 'prompt-override';
|
|
45
|
+
scope: 'global' | 'agent' | 'job';
|
|
46
|
+
scopeKey?: string;
|
|
47
|
+
content: string;
|
|
48
|
+
}
|
|
49
|
+
export type AutoApply = AutoApplyCron | AutoApplyAdvisorRule | AutoApplyPromptOverride;
|
|
30
50
|
export interface Diagnosis {
|
|
31
51
|
rootCause: string;
|
|
32
52
|
confidence: 'high' | 'medium' | 'low';
|
|
33
53
|
proposedFix: {
|
|
34
|
-
type: 'config_change' | 'prompt_change' | 'agent_scope' | 'disable' | 'credential_refresh' | 'escalate_to_owner';
|
|
54
|
+
type: 'config_change' | 'prompt_change' | 'agent_scope' | 'disable' | 'credential_refresh' | 'advisor_rule' | 'prompt_override' | 'escalate_to_owner';
|
|
35
55
|
details: string;
|
|
36
56
|
diff?: string;
|
|
37
57
|
/**
|
|
38
|
-
* When present, the fix can be applied with one click via the
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
58
|
+
* When present, the fix can be applied with one click via the dashboard's
|
|
59
|
+
* apply-fix endpoint. Three shapes (kind=cron|advisor-rule|prompt-override).
|
|
60
|
+
* Each kind has its own validator that runs in sanitizeAutoApply before
|
|
61
|
+
* the proposal is cached, and again in fix-applier before any write.
|
|
42
62
|
*/
|
|
43
|
-
autoApply?:
|
|
44
|
-
agentSlug?: string;
|
|
45
|
-
operations: FixOperation[];
|
|
46
|
-
};
|
|
63
|
+
autoApply?: AutoApply;
|
|
47
64
|
};
|
|
48
65
|
riskLevel: 'low' | 'medium' | 'high';
|
|
49
66
|
generatedAt: string;
|
|
@@ -173,28 +173,43 @@ function buildPrompt(broken, jobDef, agentProfile, recentRuns) {
|
|
|
173
173
|
'',
|
|
174
174
|
'## Auto-apply contract',
|
|
175
175
|
'',
|
|
176
|
-
'When
|
|
176
|
+
'When the fix is mechanical — set or remove a known scalar field, write a small advisor rule, or add prompt guidance — ALSO populate `proposedFix.autoApply`. The owner can one-click approve it. There are three KINDS of auto-apply, pick the one that matches:',
|
|
177
177
|
'',
|
|
178
|
-
'
|
|
178
|
+
'### kind: "cron" (default — edit CRON.md frontmatter)',
|
|
179
|
+
'Use for: tier, mode, max_hours, max_turns, max_retries, enabled, agentSlug, work_dir, model, always_deliver, after, timeout_ms.',
|
|
180
|
+
'Shape: { "kind": "cron", "agentSlug"?: "...", "operations": [...] }',
|
|
181
|
+
'Operations: { "op": "set", "field": "<name>", "value": <scalar> } or { "op": "remove", "field": "<name>" }.',
|
|
182
|
+
'If the job is agent-scoped (job name has ":"), set agentSlug to the prefix.',
|
|
183
|
+
'Examples:',
|
|
184
|
+
'- Remove unleashed + companion + cap turns: { "kind": "cron", "operations": [{"op":"remove","field":"mode"}, {"op":"remove","field":"max_hours"}, {"op":"set","field":"max_turns","value":25}] }',
|
|
185
|
+
'- Bump maxTurns: { "kind": "cron", "operations": [{"op":"set","field":"max_turns","value":10}] }',
|
|
179
186
|
'',
|
|
180
|
-
|
|
187
|
+
'### kind: "advisor-rule" (write a YAML rule to ~/.clementine/advisor-rules/user/)',
|
|
188
|
+
'Use when the fix is a behavioral rule that should affect ALL jobs matching some scope, not just one cron job. Examples: "for unleashed jobs, never bump maxTurns" or "for ross-the-sdr, always set timeout to 900s on max_turns errors".',
|
|
189
|
+
'Shape: { "kind": "advisor-rule", "ruleId": "kebab-case-id", "yamlContent": "<full yaml body>" }',
|
|
190
|
+
'The YAML body must be a valid advisor rule (schemaVersion: 1, id, description, priority, when, then). User rules at priority 100+ override builtins of the same id.',
|
|
191
|
+
'Example:',
|
|
192
|
+
'{ "kind": "advisor-rule", "ruleId": "ross-aggressive-timeout", "yamlContent": "schemaVersion: 1\\nid: ross-aggressive-timeout\\ndescription: Bump timeout for ross\\npriority: 105\\nappliesTo:\\n agentSlug: ross-the-sdr\\nwhen:\\n - kind: recentTimeoutHits\\n window: 5\\n atLeast: 1\\nthen:\\n - kind: bumpTimeoutMs\\n multiplier: 2.0" }',
|
|
181
193
|
'',
|
|
182
|
-
'
|
|
194
|
+
'### kind: "prompt-override" (write a markdown file to ~/.clementine/prompt-overrides/)',
|
|
195
|
+
'Use when the fix is "give the LLM more guidance for this job/agent". Examples: a job consistently misses an edge case, an agent needs a reminder about output format.',
|
|
196
|
+
'Shape: { "kind": "prompt-override", "scope": "job"|"agent"|"global", "scopeKey": "<job or agent name>", "content": "<markdown body>" }',
|
|
197
|
+
'For scope=global, omit scopeKey. For scope=agent, scopeKey is the agent slug. For scope=job, scopeKey is the BARE job name (no agent prefix).',
|
|
198
|
+
'Example:',
|
|
199
|
+
'{ "kind": "prompt-override", "scope": "job", "scopeKey": "market-leader-followup", "content": "If the inbox query returns 0 rows, batch the duplicate-task cleanup in groups of 50 using bash heredoc loops. Do not enumerate task IDs in the prompt." }',
|
|
183
200
|
'',
|
|
184
|
-
'
|
|
185
|
-
'
|
|
186
|
-
'- Scope a broken global job to Ross\'s profile: operations: [{"op":"set","field":"agentSlug","value":"ross-the-sdr"}]',
|
|
187
|
-
'- Bump maxTurns on an under-resourced job: operations: [{"op":"set","field":"max_turns","value":10}]',
|
|
201
|
+
'## When NOT to use autoApply',
|
|
202
|
+
'For credential refreshes, multi-line CRON.md edits beyond the scalar allowlist, or any change you are not confident about: OMIT autoApply entirely. The owner will handle those manually.',
|
|
188
203
|
'',
|
|
189
204
|
'## Output schema (JSON only, no markdown fences):',
|
|
190
205
|
'{',
|
|
191
|
-
' "rootCause": "1-2 sentences explaining WHY the job is failing
|
|
206
|
+
' "rootCause": "1-2 sentences explaining WHY the job is failing",',
|
|
192
207
|
' "confidence": "high|medium|low",',
|
|
193
208
|
' "proposedFix": {',
|
|
194
|
-
' "type": "config_change|prompt_change|agent_scope|disable|credential_refresh|escalate_to_owner",',
|
|
195
|
-
' "details": "prose description of the fix
|
|
196
|
-
' "diff": "optional:
|
|
197
|
-
' "autoApply": "optional:
|
|
209
|
+
' "type": "config_change|prompt_change|agent_scope|disable|credential_refresh|advisor_rule|prompt_override|escalate_to_owner",',
|
|
210
|
+
' "details": "prose description of the fix",',
|
|
211
|
+
' "diff": "optional: before/after diff",',
|
|
212
|
+
' "autoApply": "optional: one of the three shapes above"',
|
|
198
213
|
' },',
|
|
199
214
|
' "riskLevel": "low|medium|high"',
|
|
200
215
|
'}',
|
|
@@ -230,32 +245,42 @@ function parseResponse(raw) {
|
|
|
230
245
|
}
|
|
231
246
|
}
|
|
232
247
|
/**
|
|
233
|
-
* Strictly validate and filter autoApply.
|
|
234
|
-
*
|
|
235
|
-
* nothing valid remains.
|
|
248
|
+
* Strictly validate and filter autoApply. Dispatches on `kind` (default 'cron'
|
|
249
|
+
* for back-compat). Returns null if validation fails for the chosen kind.
|
|
236
250
|
*/
|
|
237
251
|
function sanitizeAutoApply(raw) {
|
|
238
252
|
if (!raw || typeof raw !== 'object')
|
|
239
253
|
return null;
|
|
240
254
|
const obj = raw;
|
|
255
|
+
const kind = typeof obj.kind === 'string' ? obj.kind : 'cron';
|
|
256
|
+
if (kind === 'cron')
|
|
257
|
+
return sanitizeAutoApplyCron(obj);
|
|
258
|
+
if (kind === 'advisor-rule')
|
|
259
|
+
return sanitizeAutoApplyAdvisorRule(obj);
|
|
260
|
+
if (kind === 'prompt-override')
|
|
261
|
+
return sanitizeAutoApplyPromptOverride(obj);
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
function sanitizeAutoApplyCron(raw) {
|
|
265
|
+
const obj = raw;
|
|
241
266
|
if (!Array.isArray(obj.operations))
|
|
242
267
|
return null;
|
|
243
268
|
const operations = [];
|
|
244
269
|
for (const op of obj.operations) {
|
|
245
270
|
if (!op || typeof op !== 'object')
|
|
246
271
|
continue;
|
|
247
|
-
const
|
|
248
|
-
if (typeof
|
|
272
|
+
const r = op;
|
|
273
|
+
if (typeof r.field !== 'string')
|
|
249
274
|
continue;
|
|
250
|
-
if (!EDITABLE_FIELDS.has(
|
|
275
|
+
if (!EDITABLE_FIELDS.has(r.field))
|
|
251
276
|
continue;
|
|
252
|
-
if (
|
|
253
|
-
operations.push({ op: 'remove', field:
|
|
277
|
+
if (r.op === 'remove') {
|
|
278
|
+
operations.push({ op: 'remove', field: r.field });
|
|
254
279
|
}
|
|
255
|
-
else if (
|
|
256
|
-
const v =
|
|
280
|
+
else if (r.op === 'set') {
|
|
281
|
+
const v = r.value;
|
|
257
282
|
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
|
258
|
-
operations.push({ op: 'set', field:
|
|
283
|
+
operations.push({ op: 'set', field: r.field, value: v });
|
|
259
284
|
}
|
|
260
285
|
}
|
|
261
286
|
}
|
|
@@ -264,7 +289,46 @@ function sanitizeAutoApply(raw) {
|
|
|
264
289
|
const agentSlug = typeof obj.agentSlug === 'string' && /^[a-z0-9-]+$/i.test(obj.agentSlug)
|
|
265
290
|
? obj.agentSlug
|
|
266
291
|
: undefined;
|
|
267
|
-
return agentSlug ? { agentSlug
|
|
292
|
+
return { kind: 'cron', operations, ...(agentSlug ? { agentSlug } : {}) };
|
|
293
|
+
}
|
|
294
|
+
function sanitizeAutoApplyAdvisorRule(raw) {
|
|
295
|
+
const obj = raw;
|
|
296
|
+
if (typeof obj.ruleId !== 'string' || !obj.ruleId.trim())
|
|
297
|
+
return null;
|
|
298
|
+
if (!/^[a-z0-9-]+$/.test(obj.ruleId))
|
|
299
|
+
return null; // safe filename
|
|
300
|
+
if (typeof obj.yamlContent !== 'string' || !obj.yamlContent.trim())
|
|
301
|
+
return null;
|
|
302
|
+
if (obj.yamlContent.length > 10_000)
|
|
303
|
+
return null; // sanity bound
|
|
304
|
+
return {
|
|
305
|
+
kind: 'advisor-rule',
|
|
306
|
+
ruleId: obj.ruleId,
|
|
307
|
+
yamlContent: obj.yamlContent,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function sanitizeAutoApplyPromptOverride(raw) {
|
|
311
|
+
const obj = raw;
|
|
312
|
+
if (obj.scope !== 'global' && obj.scope !== 'agent' && obj.scope !== 'job')
|
|
313
|
+
return null;
|
|
314
|
+
if (typeof obj.content !== 'string' || !obj.content.trim())
|
|
315
|
+
return null;
|
|
316
|
+
if (obj.content.length > 20_000)
|
|
317
|
+
return null; // sanity bound
|
|
318
|
+
if (obj.scope === 'global') {
|
|
319
|
+
return { kind: 'prompt-override', scope: 'global', content: obj.content };
|
|
320
|
+
}
|
|
321
|
+
// agent or job — require scopeKey, validate as safe filename
|
|
322
|
+
if (typeof obj.scopeKey !== 'string' || !obj.scopeKey)
|
|
323
|
+
return null;
|
|
324
|
+
if (!/^[a-zA-Z0-9_:-]+$/.test(obj.scopeKey))
|
|
325
|
+
return null;
|
|
326
|
+
return {
|
|
327
|
+
kind: 'prompt-override',
|
|
328
|
+
scope: obj.scope,
|
|
329
|
+
scopeKey: obj.scopeKey,
|
|
330
|
+
content: obj.content,
|
|
331
|
+
};
|
|
268
332
|
}
|
|
269
333
|
/**
|
|
270
334
|
* Diagnose one broken job. Returns a cached diagnosis if one exists and is
|
|
@@ -25,28 +25,7 @@ export interface BrokenJob {
|
|
|
25
25
|
circuitBreakerEngagedAt: string | null;
|
|
26
26
|
lastAdvisorOpinion: string | null;
|
|
27
27
|
/** Populated asynchronously by the diagnostic agent when available. */
|
|
28
|
-
diagnosis?:
|
|
29
|
-
rootCause: string;
|
|
30
|
-
confidence: 'high' | 'medium' | 'low';
|
|
31
|
-
proposedFix: {
|
|
32
|
-
type: string;
|
|
33
|
-
details: string;
|
|
34
|
-
diff?: string;
|
|
35
|
-
autoApply?: {
|
|
36
|
-
agentSlug?: string;
|
|
37
|
-
operations: Array<{
|
|
38
|
-
op: 'set';
|
|
39
|
-
field: string;
|
|
40
|
-
value: string | number | boolean;
|
|
41
|
-
} | {
|
|
42
|
-
op: 'remove';
|
|
43
|
-
field: string;
|
|
44
|
-
}>;
|
|
45
|
-
};
|
|
46
|
-
};
|
|
47
|
-
riskLevel: 'low' | 'medium' | 'high';
|
|
48
|
-
generatedAt: string;
|
|
49
|
-
};
|
|
28
|
+
diagnosis?: import('./failure-diagnostics.js').Diagnosis;
|
|
50
29
|
}
|
|
51
30
|
/**
|
|
52
31
|
* Compute the current set of broken jobs by scanning all run logs.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Every apply writes a .bak next to the CRON.md and appends to an audit
|
|
12
12
|
* log before touching the file.
|
|
13
13
|
*/
|
|
14
|
-
import { type FixOperation } from './failure-diagnostics.js';
|
|
14
|
+
import { type AutoApply, type FixOperation } from './failure-diagnostics.js';
|
|
15
15
|
export interface ApplyResult {
|
|
16
16
|
ok: boolean;
|
|
17
17
|
message: string;
|
|
@@ -20,15 +20,19 @@ export interface ApplyResult {
|
|
|
20
20
|
skippedOps?: FixOperation[];
|
|
21
21
|
diff?: string;
|
|
22
22
|
}
|
|
23
|
+
export interface ApplyOptions {
|
|
24
|
+
dryRun?: boolean;
|
|
25
|
+
/** Override BASE_DIR for advisor-rule and prompt-override write paths. Tests only. */
|
|
26
|
+
baseDir?: string;
|
|
27
|
+
}
|
|
23
28
|
/**
|
|
24
|
-
* Apply a proposed fix to the right
|
|
25
|
-
*
|
|
26
|
-
*
|
|
29
|
+
* Apply a proposed fix to the right target. Dispatches on autoApply.kind:
|
|
30
|
+
* 'cron' — edit CRON.md frontmatter (the original path)
|
|
31
|
+
* 'advisor-rule' — write a YAML rule under ~/.clementine/advisor-rules/user/
|
|
32
|
+
* 'prompt-override' — write a markdown override under ~/.clementine/prompt-overrides/
|
|
33
|
+
*
|
|
34
|
+
* Each path has its own backup/audit. All idempotent: re-applying the same
|
|
35
|
+
* fix produces the same on-disk state.
|
|
27
36
|
*/
|
|
28
|
-
export declare function applyFix(jobName: string, autoApply:
|
|
29
|
-
agentSlug?: string;
|
|
30
|
-
operations: FixOperation[];
|
|
31
|
-
}, opts?: {
|
|
32
|
-
dryRun?: boolean;
|
|
33
|
-
}): ApplyResult;
|
|
37
|
+
export declare function applyFix(jobName: string, autoApply: AutoApply, opts?: ApplyOptions): ApplyResult;
|
|
34
38
|
//# sourceMappingURL=fix-applier.d.ts.map
|
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
* Every apply writes a .bak next to the CRON.md and appends to an audit
|
|
12
12
|
* log before touching the file.
|
|
13
13
|
*/
|
|
14
|
-
import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
14
|
+
import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import pino from 'pino';
|
|
17
|
+
import yaml from 'js-yaml';
|
|
17
18
|
import { AGENTS_DIR, BASE_DIR, CRON_FILE } from '../config.js';
|
|
18
|
-
import { EDITABLE_FIELDS } from './failure-diagnostics.js';
|
|
19
|
+
import { EDITABLE_FIELDS, } from './failure-diagnostics.js';
|
|
19
20
|
const logger = pino({ name: 'clementine.fix-applier' });
|
|
20
21
|
const AUDIT_FILE = path.join(BASE_DIR, 'cron', 'fix-applier.log');
|
|
21
22
|
/**
|
|
@@ -232,11 +233,26 @@ function findBlockEnd(lines, start) {
|
|
|
232
233
|
return lines.length;
|
|
233
234
|
}
|
|
234
235
|
/**
|
|
235
|
-
* Apply a proposed fix to the right
|
|
236
|
-
*
|
|
237
|
-
*
|
|
236
|
+
* Apply a proposed fix to the right target. Dispatches on autoApply.kind:
|
|
237
|
+
* 'cron' — edit CRON.md frontmatter (the original path)
|
|
238
|
+
* 'advisor-rule' — write a YAML rule under ~/.clementine/advisor-rules/user/
|
|
239
|
+
* 'prompt-override' — write a markdown override under ~/.clementine/prompt-overrides/
|
|
240
|
+
*
|
|
241
|
+
* Each path has its own backup/audit. All idempotent: re-applying the same
|
|
242
|
+
* fix produces the same on-disk state.
|
|
238
243
|
*/
|
|
239
244
|
export function applyFix(jobName, autoApply, opts = {}) {
|
|
245
|
+
// Default 'cron' for back-compat with old AutoApplyCron objects without kind.
|
|
246
|
+
const kind = autoApply.kind ?? 'cron';
|
|
247
|
+
if (kind === 'cron')
|
|
248
|
+
return applyCronFix(jobName, autoApply, opts);
|
|
249
|
+
if (kind === 'advisor-rule')
|
|
250
|
+
return applyAdvisorRuleFix(jobName, autoApply, opts);
|
|
251
|
+
if (kind === 'prompt-override')
|
|
252
|
+
return applyPromptOverrideFix(jobName, autoApply, opts);
|
|
253
|
+
return { ok: false, message: `Unknown autoApply.kind: ${String(kind)}` };
|
|
254
|
+
}
|
|
255
|
+
function applyCronFix(jobName, autoApply, opts) {
|
|
240
256
|
const cronFile = resolveCronFile(jobName, autoApply);
|
|
241
257
|
if (!cronFile) {
|
|
242
258
|
return { ok: false, message: `No CRON.md found for ${jobName}` };
|
|
@@ -280,6 +296,7 @@ export function applyFix(jobName, autoApply, opts = {}) {
|
|
|
280
296
|
const newContent = newLines.join('\n');
|
|
281
297
|
writeFileSync(cronFile, newContent);
|
|
282
298
|
appendAudit({
|
|
299
|
+
kind: 'cron',
|
|
283
300
|
jobName,
|
|
284
301
|
file: cronFile,
|
|
285
302
|
applied,
|
|
@@ -296,6 +313,108 @@ export function applyFix(jobName, autoApply, opts = {}) {
|
|
|
296
313
|
diff,
|
|
297
314
|
};
|
|
298
315
|
}
|
|
316
|
+
// ── Advisor rule writer ──────────────────────────────────────────────
|
|
317
|
+
function userRulesDir(baseDir) {
|
|
318
|
+
return path.join(baseDir, 'advisor-rules', 'user');
|
|
319
|
+
}
|
|
320
|
+
function applyAdvisorRuleFix(jobName, autoApply, opts) {
|
|
321
|
+
const targetDir = userRulesDir(opts.baseDir ?? BASE_DIR);
|
|
322
|
+
// Validate that the YAML parses and has the minimum schema shape. Don't
|
|
323
|
+
// require full Phase 2 zod validation here — the loader will reject invalid
|
|
324
|
+
// rules at read time and the next reload — but catch obvious malformed input.
|
|
325
|
+
let parsed;
|
|
326
|
+
try {
|
|
327
|
+
parsed = yaml.load(autoApply.yamlContent);
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
return { ok: false, message: `Invalid YAML in advisor-rule body: ${String(err)}` };
|
|
331
|
+
}
|
|
332
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
333
|
+
return { ok: false, message: 'advisor-rule yamlContent did not parse as a YAML object' };
|
|
334
|
+
}
|
|
335
|
+
const r = parsed;
|
|
336
|
+
if (r.schemaVersion !== 1) {
|
|
337
|
+
return { ok: false, message: 'advisor-rule must declare schemaVersion: 1' };
|
|
338
|
+
}
|
|
339
|
+
if (typeof r.id !== 'string' || r.id !== autoApply.ruleId) {
|
|
340
|
+
return { ok: false, message: `advisor-rule yamlContent id must match ruleId="${autoApply.ruleId}"` };
|
|
341
|
+
}
|
|
342
|
+
if (!Array.isArray(r.when) || !Array.isArray(r.then)) {
|
|
343
|
+
return { ok: false, message: 'advisor-rule must have when[] and then[] arrays' };
|
|
344
|
+
}
|
|
345
|
+
const targetPath = path.join(targetDir, `${autoApply.ruleId}.yaml`);
|
|
346
|
+
const diff = `+ advisor-rule ${autoApply.ruleId} → ${targetPath}`;
|
|
347
|
+
if (opts.dryRun) {
|
|
348
|
+
return { ok: true, message: 'Dry run: advisor-rule would be written', file: targetPath, diff };
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
mkdirSync(targetDir, { recursive: true });
|
|
352
|
+
const tmp = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
|
|
353
|
+
writeFileSync(tmp, autoApply.yamlContent);
|
|
354
|
+
renameSync(tmp, targetPath);
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
return { ok: false, message: `Failed to write advisor rule: ${String(err)}` };
|
|
358
|
+
}
|
|
359
|
+
appendAudit({ kind: 'advisor-rule', jobName, file: targetPath, ruleId: autoApply.ruleId, diff });
|
|
360
|
+
logger.info({ jobName, ruleId: autoApply.ruleId, file: targetPath }, 'Applied advisor-rule fix');
|
|
361
|
+
return {
|
|
362
|
+
ok: true,
|
|
363
|
+
message: `Wrote advisor rule ${autoApply.ruleId} (hot-reloads on next eval)`,
|
|
364
|
+
file: targetPath,
|
|
365
|
+
diff,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
// ── Prompt override writer ───────────────────────────────────────────
|
|
369
|
+
function promptOverridesDir(baseDir) {
|
|
370
|
+
return path.join(baseDir, 'prompt-overrides');
|
|
371
|
+
}
|
|
372
|
+
function applyPromptOverrideFix(jobName, autoApply, opts) {
|
|
373
|
+
const root = promptOverridesDir(opts.baseDir ?? BASE_DIR);
|
|
374
|
+
// Resolve target path from scope.
|
|
375
|
+
let targetPath;
|
|
376
|
+
if (autoApply.scope === 'global') {
|
|
377
|
+
targetPath = path.join(root, '_global.md');
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
if (!autoApply.scopeKey) {
|
|
381
|
+
return { ok: false, message: `prompt-override scope=${autoApply.scope} requires scopeKey` };
|
|
382
|
+
}
|
|
383
|
+
if (/[\/\\\.]/.test(autoApply.scopeKey)) {
|
|
384
|
+
return { ok: false, message: 'prompt-override scopeKey cannot contain "/", "\\", or "."' };
|
|
385
|
+
}
|
|
386
|
+
const sub = autoApply.scope === 'agent' ? 'agents' : 'jobs';
|
|
387
|
+
targetPath = path.join(root, sub, `${autoApply.scopeKey}.md`);
|
|
388
|
+
}
|
|
389
|
+
const diff = `+ prompt-override ${autoApply.scope}${autoApply.scopeKey ? `:${autoApply.scopeKey}` : ''} → ${targetPath}`;
|
|
390
|
+
if (opts.dryRun) {
|
|
391
|
+
return { ok: true, message: 'Dry run: prompt-override would be written', file: targetPath, diff };
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
395
|
+
const tmp = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
|
|
396
|
+
writeFileSync(tmp, autoApply.content);
|
|
397
|
+
renameSync(tmp, targetPath);
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
return { ok: false, message: `Failed to write prompt override: ${String(err)}` };
|
|
401
|
+
}
|
|
402
|
+
appendAudit({
|
|
403
|
+
kind: 'prompt-override',
|
|
404
|
+
jobName,
|
|
405
|
+
file: targetPath,
|
|
406
|
+
scope: autoApply.scope,
|
|
407
|
+
scopeKey: autoApply.scopeKey,
|
|
408
|
+
diff,
|
|
409
|
+
});
|
|
410
|
+
logger.info({ jobName, scope: autoApply.scope, scopeKey: autoApply.scopeKey, file: targetPath }, 'Applied prompt-override fix');
|
|
411
|
+
return {
|
|
412
|
+
ok: true,
|
|
413
|
+
message: `Wrote prompt override ${autoApply.scope}${autoApply.scopeKey ? `:${autoApply.scopeKey}` : ''}`,
|
|
414
|
+
file: targetPath,
|
|
415
|
+
diff,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
299
418
|
function appendAudit(entry) {
|
|
300
419
|
try {
|
|
301
420
|
mkdirSync(path.dirname(AUDIT_FILE), { recursive: true });
|
|
@@ -1671,6 +1671,72 @@ export function registerAdminTools(server) {
|
|
|
1671
1671
|
`Reason: ${reason}\n\n` +
|
|
1672
1672
|
`The daemon will validate in a staging worktree, then commit + build + restart if compilation succeeds.`);
|
|
1673
1673
|
});
|
|
1674
|
+
// ── Prompt Overrides ────────────────────────────────────────────────────
|
|
1675
|
+
server.tool('prompt_override_write', 'Write a markdown file under ~/.clementine/prompt-overrides/ that gets prepended to a job\'s prompt at runtime. Use this when you identify that a specific cron job, agent, or all jobs need additional guidance, context, or constraints. Survives `npm update -g clementine` and reloads in <1s. Scopes: "global" (every job), "agent" (every job for one agent), "job" (one specific job).', {
|
|
1676
|
+
scope: z.enum(['global', 'agent', 'job']).describe('Which jobs the override applies to'),
|
|
1677
|
+
scopeKey: z.string().optional().describe('For scope=agent: the agent slug. For scope=job: the job name. Ignored for scope=global.'),
|
|
1678
|
+
content: z.string().min(1).describe('Markdown body to prepend to the prompt. May include optional gray-matter frontmatter for priority/position.'),
|
|
1679
|
+
reason: z.string().describe('Why this override is being written — for the audit log.'),
|
|
1680
|
+
}, async ({ scope, scopeKey, content, reason }) => {
|
|
1681
|
+
if ((scope === 'agent' || scope === 'job') && !scopeKey) {
|
|
1682
|
+
return textResult(`Error: scope="${scope}" requires scopeKey to be provided.`);
|
|
1683
|
+
}
|
|
1684
|
+
if (scopeKey && /[\/\\\.]/.test(scopeKey)) {
|
|
1685
|
+
return textResult('Error: scopeKey cannot contain "/", "\\", or "."');
|
|
1686
|
+
}
|
|
1687
|
+
const ROOT = path.join(BASE_DIR, 'prompt-overrides');
|
|
1688
|
+
let targetPath;
|
|
1689
|
+
if (scope === 'global') {
|
|
1690
|
+
targetPath = path.join(ROOT, '_global.md');
|
|
1691
|
+
}
|
|
1692
|
+
else if (scope === 'agent') {
|
|
1693
|
+
targetPath = path.join(ROOT, 'agents', `${scopeKey}.md`);
|
|
1694
|
+
}
|
|
1695
|
+
else {
|
|
1696
|
+
targetPath = path.join(ROOT, 'jobs', `${scopeKey}.md`);
|
|
1697
|
+
}
|
|
1698
|
+
try {
|
|
1699
|
+
mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
1700
|
+
const tmp = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
|
|
1701
|
+
writeFileSync(tmp, content);
|
|
1702
|
+
renameSync(tmp, targetPath);
|
|
1703
|
+
logger.info({ scope, scopeKey, targetPath, reason, contentLen: content.length }, 'Wrote prompt override');
|
|
1704
|
+
return textResult(`Prompt override written.\n` +
|
|
1705
|
+
`Scope: ${scope}${scopeKey ? ` (${scopeKey})` : ''}\n` +
|
|
1706
|
+
`Path: ${targetPath}\n` +
|
|
1707
|
+
`Reason: ${reason}\n\n` +
|
|
1708
|
+
`The override will be picked up on the next job run (hot-reloads via fs.watch within ~1s).`);
|
|
1709
|
+
}
|
|
1710
|
+
catch (err) {
|
|
1711
|
+
logger.error({ err, scope, scopeKey, targetPath }, 'Failed to write prompt override');
|
|
1712
|
+
return textResult(`Failed to write prompt override: ${String(err)}`);
|
|
1713
|
+
}
|
|
1714
|
+
});
|
|
1715
|
+
server.tool('prompt_override_list', 'List all prompt overrides currently active in ~/.clementine/prompt-overrides/, grouped by scope. Use this to see what overrides exist before writing or removing one.', {}, async () => {
|
|
1716
|
+
const ROOT = path.join(BASE_DIR, 'prompt-overrides');
|
|
1717
|
+
if (!existsSync(ROOT)) {
|
|
1718
|
+
return textResult('No prompt-overrides directory yet. Use prompt_override_write to create one.');
|
|
1719
|
+
}
|
|
1720
|
+
const lines = ['## Prompt Overrides'];
|
|
1721
|
+
const globalPath = path.join(ROOT, '_global.md');
|
|
1722
|
+
if (existsSync(globalPath))
|
|
1723
|
+
lines.push(`- global: _global.md`);
|
|
1724
|
+
const agentsDir = path.join(ROOT, 'agents');
|
|
1725
|
+
if (existsSync(agentsDir)) {
|
|
1726
|
+
for (const f of readdirSync(agentsDir).filter(f => f.endsWith('.md'))) {
|
|
1727
|
+
lines.push(`- agent: ${path.basename(f, '.md')}`);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
const jobsDir = path.join(ROOT, 'jobs');
|
|
1731
|
+
if (existsSync(jobsDir)) {
|
|
1732
|
+
for (const f of readdirSync(jobsDir).filter(f => f.endsWith('.md'))) {
|
|
1733
|
+
lines.push(`- job: ${path.basename(f, '.md')}`);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
if (lines.length === 1)
|
|
1737
|
+
lines.push('(none)');
|
|
1738
|
+
return textResult(lines.join('\n'));
|
|
1739
|
+
});
|
|
1674
1740
|
server.tool('update_self', 'Check for and apply upstream code updates. Can check without applying, or check and apply in one step.', {
|
|
1675
1741
|
action: z.enum(['check', 'apply']).describe('"check" to see if updates are available, "apply" to pull and restart'),
|
|
1676
1742
|
}, async ({ action }) => {
|