clementine-agent 1.0.95 → 1.0.96

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.
@@ -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)
@@ -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 }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.0.95",
3
+ "version": "1.0.96",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",