clementine-agent 1.0.96 → 1.0.98
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/self-improve.d.ts +7 -0
- package/dist/agent/self-improve.js +114 -71
- 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/types.d.ts +2 -2
- package/package.json +1 -1
|
@@ -59,8 +59,15 @@ export declare class SelfImproveLoop {
|
|
|
59
59
|
private parseJsonResponse;
|
|
60
60
|
private withTimeout;
|
|
61
61
|
}
|
|
62
|
+
/** Validate that a proposed change has valid syntax for its target area. */
|
|
62
63
|
export declare function validateProposal(area: string, target: string, proposedChange: string): {
|
|
63
64
|
valid: boolean;
|
|
64
65
|
error?: string;
|
|
65
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the on-disk path for a prompt-override `target` string.
|
|
69
|
+
* Accepted forms: 'global', 'agent:<slug>', 'job:<jobName>'. Returns null
|
|
70
|
+
* for malformed targets.
|
|
71
|
+
*/
|
|
72
|
+
export declare function promptOverridePathForTarget(target: string): string | null;
|
|
66
73
|
//# sourceMappingURL=self-improve.d.ts.map
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
import { randomBytes } from 'node:crypto';
|
|
12
12
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
13
13
|
import matter from 'gray-matter';
|
|
14
|
+
import { load as yamlLoad } from 'js-yaml';
|
|
14
15
|
import path from 'node:path';
|
|
15
16
|
import pino from 'pino';
|
|
16
|
-
import { BASE_DIR, SELF_IMPROVE_DIR, SOUL_FILE, AGENTS_FILE, CRON_FILE, WORKFLOWS_DIR, VAULT_DIR, MEMORY_DB_PATH, AGENTS_DIR,
|
|
17
|
+
import { BASE_DIR, SELF_IMPROVE_DIR, SOUL_FILE, AGENTS_FILE, CRON_FILE, WORKFLOWS_DIR, VAULT_DIR, MEMORY_DB_PATH, AGENTS_DIR, CRON_REFLECTIONS_DIR, GOALS_DIR, } from '../config.js';
|
|
17
18
|
import { listAllGoals } from '../tools/shared.js';
|
|
18
19
|
const logger = pino({ name: 'clementine.self-improve' });
|
|
19
20
|
// ── Defaults ─────────────────────────────────────────────────────────
|
|
@@ -23,9 +24,13 @@ const DEFAULT_CONFIG = {
|
|
|
23
24
|
maxDurationMs: 3_600_000, // 1 hour
|
|
24
25
|
acceptThreshold: 0.7,
|
|
25
26
|
plateauLimit: 3,
|
|
26
|
-
// 'source' deprecated — self-improvement
|
|
27
|
-
//
|
|
28
|
-
|
|
27
|
+
// 'source' deprecated — self-improvement produces data, not engine TS edits.
|
|
28
|
+
// 'advisor-rule' writes YAML to ~/.clementine/advisor-rules/user/.
|
|
29
|
+
// 'prompt-override' writes markdown to ~/.clementine/prompt-overrides/.
|
|
30
|
+
areas: [
|
|
31
|
+
'soul', 'cron', 'workflow', 'memory', 'agent', 'communication', 'goal',
|
|
32
|
+
'advisor-rule', 'prompt-override',
|
|
33
|
+
],
|
|
29
34
|
autoApply: true,
|
|
30
35
|
sourceMode: 'skip',
|
|
31
36
|
};
|
|
@@ -132,20 +137,22 @@ function checkDrift(proposedContent) {
|
|
|
132
137
|
return { ok: similarity >= DRIFT_SIMILARITY_THRESHOLD, similarity };
|
|
133
138
|
}
|
|
134
139
|
/** Classify the risk level of a proposed change.
|
|
135
|
-
* - low: agent prompts, individual cron job prompts
|
|
140
|
+
* - low: agent prompts, individual cron job prompts, advisor rules, prompt overrides
|
|
136
141
|
* - medium: SOUL.md, AGENTS.md, MEMORY.md — needs owner approval
|
|
137
|
-
* - high: source code — stays blocked
|
|
142
|
+
* - high: source code — stays blocked (deprecated path; kept for back-compat)
|
|
138
143
|
*/
|
|
139
144
|
function classifyRisk(area) {
|
|
140
145
|
switch (area) {
|
|
141
|
-
case 'agent': return 'low';
|
|
142
|
-
case 'cron': return 'low';
|
|
143
|
-
case 'workflow': return 'low';
|
|
146
|
+
case 'agent': return 'low';
|
|
147
|
+
case 'cron': return 'low';
|
|
148
|
+
case 'workflow': return 'low';
|
|
149
|
+
case 'advisor-rule': return 'low'; // YAML files, hot-reloaded, easily deleted
|
|
150
|
+
case 'prompt-override': return 'low'; // Markdown files, hot-reloaded, easily deleted
|
|
144
151
|
case 'soul': return 'medium'; // Core personality — needs approval
|
|
145
152
|
case 'communication': return 'medium'; // Global operating instructions
|
|
146
153
|
case 'memory': return 'medium'; // Memory config
|
|
147
|
-
case 'source': return 'high'; // Code changes — always blocked in auto mode
|
|
148
154
|
case 'goal': return 'medium'; // New goals need owner review before activating
|
|
155
|
+
case 'source': return 'high'; // Deprecated — quarantined in Phase 1
|
|
149
156
|
default: return 'high';
|
|
150
157
|
}
|
|
151
158
|
}
|
|
@@ -836,7 +843,15 @@ export class SelfImproveLoop {
|
|
|
836
843
|
`Area notes:\n` +
|
|
837
844
|
`- For "goal": target = "{owner}/{goal-slug}" (e.g. "clementine/improve-reply-rates" or "ross-the-sdr/book-demos"). ` +
|
|
838
845
|
`Propose when you observe a pattern in completed tasks or cron runs that suggests a missing or stale goal. ` +
|
|
839
|
-
`The proposedChange must be a JSON goal object with at minimum: title, description, priority, reviewFrequency.\n
|
|
846
|
+
`The proposedChange must be a JSON goal object with at minimum: title, description, priority, reviewFrequency.\n` +
|
|
847
|
+
`- For "advisor-rule": target = ruleId in kebab-case (e.g. "skip-turn-bump-on-unleashed"). ` +
|
|
848
|
+
`Use when the fix is a behavioral rule that affects ALL jobs matching some scope, not just one cron job. ` +
|
|
849
|
+
`Examples: "for unleashed jobs, never bump maxTurns" or "for ross-the-sdr, double timeout on max_turns". ` +
|
|
850
|
+
`The proposedChange must be a full advisor rule YAML body with: schemaVersion: 1, id (must match target), description, priority (use 100+ to override builtins), appliesTo, when[], then[]. ` +
|
|
851
|
+
`User rules at priority 100+ override engine builtins of the same id.\n` +
|
|
852
|
+
`- For "prompt-override": target = "global", "agent:<slug>", or "job:<jobName>" (e.g. "job:market-leader-followup"). ` +
|
|
853
|
+
`Use when a job/agent needs more standing guidance — markdown that gets prepended to its prompt. ` +
|
|
854
|
+
`The proposedChange is the markdown body (optionally with gray-matter frontmatter for priority/position).\n\n` +
|
|
840
855
|
`Return your answer as a JSON object matching the schema: { "results": [ ... ] }. Up to 3 items. If absolutely nothing actionable today, return { "results": [] }.`;
|
|
841
856
|
const analysisResult = await this.assistant.runPlanStep('si-analyze', analysisPrompt, {
|
|
842
857
|
tier: 2,
|
|
@@ -915,10 +930,6 @@ export class SelfImproveLoop {
|
|
|
915
930
|
const agentFile = path.join(AGENTS_DIR, target, 'agent.md');
|
|
916
931
|
return existsSync(agentFile) ? readFileSync(agentFile, 'utf-8') : '';
|
|
917
932
|
}
|
|
918
|
-
case 'source': {
|
|
919
|
-
const srcFile = path.join(PKG_DIR, 'src', target);
|
|
920
|
-
return existsSync(srcFile) ? readFileSync(srcFile, 'utf-8') : '';
|
|
921
|
-
}
|
|
922
933
|
case 'communication':
|
|
923
934
|
return existsSync(AGENTS_FILE) ? readFileSync(AGENTS_FILE, 'utf-8') : '';
|
|
924
935
|
case 'memory': {
|
|
@@ -946,6 +957,23 @@ export class SelfImproveLoop {
|
|
|
946
957
|
return '(no goals yet for this owner)';
|
|
947
958
|
return goals.map((g) => `[${g.status ?? 'unknown'}] ${g.title}: ${(g.description ?? '').slice(0, 120)}`).join('\n');
|
|
948
959
|
}
|
|
960
|
+
case 'advisor-rule': {
|
|
961
|
+
// target = ruleId (kebab-case). Show user override file if present, else builtin.
|
|
962
|
+
const userPath = path.join(BASE_DIR, 'advisor-rules', 'user', `${target}.yaml`);
|
|
963
|
+
if (existsSync(userPath))
|
|
964
|
+
return readFileSync(userPath, 'utf-8');
|
|
965
|
+
const builtinPath = path.join(BASE_DIR, 'advisor-rules', 'builtin', `${target}.yaml`);
|
|
966
|
+
if (existsSync(builtinPath))
|
|
967
|
+
return readFileSync(builtinPath, 'utf-8');
|
|
968
|
+
return '(no existing advisor rule for this id — proposing a new one)';
|
|
969
|
+
}
|
|
970
|
+
case 'prompt-override': {
|
|
971
|
+
// target = 'global' | 'agent:<slug>' | 'job:<jobName>'
|
|
972
|
+
const filePath = promptOverridePathForTarget(target);
|
|
973
|
+
if (filePath && existsSync(filePath))
|
|
974
|
+
return readFileSync(filePath, 'utf-8');
|
|
975
|
+
return '(no existing prompt override for this scope — proposing a new one)';
|
|
976
|
+
}
|
|
949
977
|
default:
|
|
950
978
|
return '';
|
|
951
979
|
}
|
|
@@ -1011,38 +1039,10 @@ export class SelfImproveLoop {
|
|
|
1011
1039
|
if (!targetPath) {
|
|
1012
1040
|
return `Cannot resolve target path for area=${pending.area}, target=${pending.target}`;
|
|
1013
1041
|
}
|
|
1014
|
-
//
|
|
1042
|
+
// 'source' area is deprecated (Phase 1 quarantine). Reject up-front so a
|
|
1043
|
+
// misbehaving proposal cannot reach the safeSourceEdit primitive.
|
|
1015
1044
|
if (pending.area === 'source') {
|
|
1016
|
-
|
|
1017
|
-
const result = await safeSourceEdit(PKG_DIR, [
|
|
1018
|
-
{ relativePath: `src/${pending.target}`, content: pending.proposedChange },
|
|
1019
|
-
], { experimentId, reason: `self-improve: ${pending.hypothesis.slice(0, 60)}`, description: pending.hypothesis });
|
|
1020
|
-
if (!result.success) {
|
|
1021
|
-
return `Source edit failed: ${result.error}${result.preflightErrors ? '\n' + result.preflightErrors.join('\n') : ''}`;
|
|
1022
|
-
}
|
|
1023
|
-
// Update experiment log — mark as approved
|
|
1024
|
-
this.updateExperimentStatus(experimentId, 'approved');
|
|
1025
|
-
try {
|
|
1026
|
-
unlinkSync(pendingFile);
|
|
1027
|
-
}
|
|
1028
|
-
catch { /* ignore */ }
|
|
1029
|
-
const state = this.loadState();
|
|
1030
|
-
state.pendingApprovals = Math.max(0, state.pendingApprovals - 1);
|
|
1031
|
-
this.saveState(state);
|
|
1032
|
-
// Schedule impact measurement for 24h later
|
|
1033
|
-
try {
|
|
1034
|
-
appendFileSync(IMPACT_CHECKS_FILE, JSON.stringify({
|
|
1035
|
-
experimentId,
|
|
1036
|
-
area: pending.area,
|
|
1037
|
-
target: pending.target,
|
|
1038
|
-
appliedAt: new Date().toISOString(),
|
|
1039
|
-
checkAfterMs: 24 * 60 * 60 * 1000,
|
|
1040
|
-
}) + '\n');
|
|
1041
|
-
}
|
|
1042
|
-
catch (err) {
|
|
1043
|
-
logger.warn({ err }, 'Failed to schedule impact check');
|
|
1044
|
-
}
|
|
1045
|
-
return `Applied source change to ${pending.target} — restart triggered.`;
|
|
1045
|
+
return 'source area is deprecated — propose advisor-rule or prompt-override instead';
|
|
1046
1046
|
}
|
|
1047
1047
|
// Goal area: parse JSON, inject required fields, ensure parent dir exists
|
|
1048
1048
|
if (pending.area === 'goal') {
|
|
@@ -1095,6 +1095,7 @@ export class SelfImproveLoop {
|
|
|
1095
1095
|
}
|
|
1096
1096
|
}
|
|
1097
1097
|
// Write the change (non-source areas)
|
|
1098
|
+
mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
1098
1099
|
writeFileSync(targetPath, pending.proposedChange);
|
|
1099
1100
|
// Record version for rollback lineage
|
|
1100
1101
|
this.recordVersion(experimentId, pending.area, pending.target, pending.hypothesis, pending.before);
|
|
@@ -1680,9 +1681,6 @@ export class SelfImproveLoop {
|
|
|
1680
1681
|
case 'agent': {
|
|
1681
1682
|
return path.join(AGENTS_DIR, target, 'agent.md');
|
|
1682
1683
|
}
|
|
1683
|
-
case 'source': {
|
|
1684
|
-
return path.join(PKG_DIR, 'src', target);
|
|
1685
|
-
}
|
|
1686
1684
|
case 'communication':
|
|
1687
1685
|
return AGENTS_FILE;
|
|
1688
1686
|
case 'memory':
|
|
@@ -1696,6 +1694,12 @@ export class SelfImproveLoop {
|
|
|
1696
1694
|
return path.join(GOALS_DIR, `${goalSlug}.json`);
|
|
1697
1695
|
return path.join(AGENTS_DIR, owner, 'goals', `${goalSlug}.json`);
|
|
1698
1696
|
}
|
|
1697
|
+
case 'advisor-rule':
|
|
1698
|
+
if (!/^[a-z0-9-]+$/.test(target))
|
|
1699
|
+
return null;
|
|
1700
|
+
return path.join(BASE_DIR, 'advisor-rules', 'user', `${target}.yaml`);
|
|
1701
|
+
case 'prompt-override':
|
|
1702
|
+
return promptOverridePathForTarget(target);
|
|
1699
1703
|
default:
|
|
1700
1704
|
return null;
|
|
1701
1705
|
}
|
|
@@ -1733,22 +1737,6 @@ export class SelfImproveLoop {
|
|
|
1733
1737
|
}
|
|
1734
1738
|
// ── Utility ──────────────────────────────────────────────────────────
|
|
1735
1739
|
/** Validate that a proposed change has valid syntax for its target area. */
|
|
1736
|
-
/** Files that must never be modified by self-improvement (catastrophic blast radius or self-referential). */
|
|
1737
|
-
const SOURCE_BLOCKLIST = new Set([
|
|
1738
|
-
'config.ts',
|
|
1739
|
-
'types.ts',
|
|
1740
|
-
'gateway/router.ts',
|
|
1741
|
-
'gateway/lanes.ts',
|
|
1742
|
-
'gateway/heartbeat-scheduler.ts',
|
|
1743
|
-
'gateway/cron-scheduler.ts',
|
|
1744
|
-
'gateway/security-scanner.ts',
|
|
1745
|
-
'agent/self-improve.ts',
|
|
1746
|
-
'agent/safe-restart.ts',
|
|
1747
|
-
'agent/source-mods.ts',
|
|
1748
|
-
'cli/index.ts',
|
|
1749
|
-
'cli/dashboard.ts',
|
|
1750
|
-
'security/scanner.ts',
|
|
1751
|
-
]);
|
|
1752
1740
|
export function validateProposal(area, target, proposedChange) {
|
|
1753
1741
|
if (!proposedChange.trim()) {
|
|
1754
1742
|
return { valid: false, error: 'Proposed change is empty' };
|
|
@@ -1790,16 +1778,71 @@ export function validateProposal(area, target, proposedChange) {
|
|
|
1790
1778
|
}
|
|
1791
1779
|
}
|
|
1792
1780
|
if (area === 'source') {
|
|
1793
|
-
//
|
|
1794
|
-
|
|
1795
|
-
|
|
1781
|
+
// Deprecated — Phase 1 quarantined source self-edit. Reject up front so
|
|
1782
|
+
// a misbehaving LLM proposal doesn't even get cached.
|
|
1783
|
+
return { valid: false, error: 'source area is deprecated; propose advisor-rule or prompt-override instead' };
|
|
1784
|
+
}
|
|
1785
|
+
if (area === 'advisor-rule') {
|
|
1786
|
+
// Must parse as YAML and have schemaVersion: 1, id matching target, when[], then[].
|
|
1787
|
+
if (!/^[a-z0-9-]+$/.test(target)) {
|
|
1788
|
+
return { valid: false, error: `advisor-rule target must be kebab-case (got "${target}")` };
|
|
1789
|
+
}
|
|
1790
|
+
let parsed;
|
|
1791
|
+
try {
|
|
1792
|
+
parsed = yamlLoad(proposedChange);
|
|
1793
|
+
}
|
|
1794
|
+
catch (err) {
|
|
1795
|
+
return { valid: false, error: `advisor-rule YAML parse error: ${err instanceof Error ? err.message : String(err)}` };
|
|
1796
|
+
}
|
|
1797
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
1798
|
+
return { valid: false, error: 'advisor-rule body did not parse as a YAML object' };
|
|
1799
|
+
}
|
|
1800
|
+
const r = parsed;
|
|
1801
|
+
if (r.schemaVersion !== 1)
|
|
1802
|
+
return { valid: false, error: 'advisor-rule must declare schemaVersion: 1' };
|
|
1803
|
+
if (typeof r.id !== 'string' || r.id !== target) {
|
|
1804
|
+
return { valid: false, error: `advisor-rule id must match target ("${target}")` };
|
|
1805
|
+
}
|
|
1806
|
+
if (!Array.isArray(r.when) || !Array.isArray(r.then)) {
|
|
1807
|
+
return { valid: false, error: 'advisor-rule must have when[] and then[] arrays' };
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
if (area === 'prompt-override') {
|
|
1811
|
+
// target format: 'global' | 'agent:<slug>' | 'job:<jobName>'
|
|
1812
|
+
const path = promptOverridePathForTarget(target);
|
|
1813
|
+
if (!path) {
|
|
1814
|
+
return { valid: false, error: `prompt-override target must be 'global', 'agent:<slug>', or 'job:<jobName>' (got "${target}")` };
|
|
1815
|
+
}
|
|
1816
|
+
if (proposedChange.length > 20_000) {
|
|
1817
|
+
return { valid: false, error: 'prompt-override content exceeds 20KB sanity bound' };
|
|
1796
1818
|
}
|
|
1797
|
-
// Size sanity: reject wholesale rewrites (proposed content > 2x original would be caught by caller).
|
|
1798
|
-
// Source proposals may be small patches or modules without import/export statements;
|
|
1799
|
-
// callers that apply source changes do the syntax-aware validation.
|
|
1800
1819
|
}
|
|
1801
1820
|
return { valid: true };
|
|
1802
1821
|
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Resolve the on-disk path for a prompt-override `target` string.
|
|
1824
|
+
* Accepted forms: 'global', 'agent:<slug>', 'job:<jobName>'. Returns null
|
|
1825
|
+
* for malformed targets.
|
|
1826
|
+
*/
|
|
1827
|
+
export function promptOverridePathForTarget(target) {
|
|
1828
|
+
const root = path.join(BASE_DIR, 'prompt-overrides');
|
|
1829
|
+
if (target === 'global')
|
|
1830
|
+
return path.join(root, '_global.md');
|
|
1831
|
+
const idx = target.indexOf(':');
|
|
1832
|
+
if (idx <= 0)
|
|
1833
|
+
return null;
|
|
1834
|
+
const scope = target.slice(0, idx);
|
|
1835
|
+
const key = target.slice(idx + 1);
|
|
1836
|
+
if (!key)
|
|
1837
|
+
return null;
|
|
1838
|
+
if (/[\/\\\.]/.test(key))
|
|
1839
|
+
return null;
|
|
1840
|
+
if (scope === 'agent')
|
|
1841
|
+
return path.join(root, 'agents', `${key}.md`);
|
|
1842
|
+
if (scope === 'job')
|
|
1843
|
+
return path.join(root, 'jobs', `${key}.md`);
|
|
1844
|
+
return null;
|
|
1845
|
+
}
|
|
1803
1846
|
function ensureDirs() {
|
|
1804
1847
|
for (const dir of [SELF_IMPROVE_DIR, PENDING_DIR]) {
|
|
1805
1848
|
if (!existsSync(dir)) {
|
|
@@ -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 });
|
package/dist/types.d.ts
CHANGED
|
@@ -501,7 +501,7 @@ export interface SelfImproveExperiment {
|
|
|
501
501
|
startedAt: string;
|
|
502
502
|
finishedAt: string;
|
|
503
503
|
durationMs: number;
|
|
504
|
-
area: 'soul' | 'cron' | 'workflow' | 'memory' | 'agent' | 'source' | 'communication' | 'goal';
|
|
504
|
+
area: 'soul' | 'cron' | 'workflow' | 'memory' | 'agent' | 'source' | 'communication' | 'goal' | 'advisor-rule' | 'prompt-override';
|
|
505
505
|
target: string;
|
|
506
506
|
hypothesis: string;
|
|
507
507
|
proposedChange: string;
|
|
@@ -549,7 +549,7 @@ export interface SelfImproveConfig {
|
|
|
549
549
|
maxDurationMs: number;
|
|
550
550
|
acceptThreshold: number;
|
|
551
551
|
plateauLimit: number;
|
|
552
|
-
areas: ('soul' | 'cron' | 'workflow' | 'memory' | 'agent' | 'source' | 'communication' | 'goal')[];
|
|
552
|
+
areas: ('soul' | 'cron' | 'workflow' | 'memory' | 'agent' | 'source' | 'communication' | 'goal' | 'advisor-rule' | 'prompt-override')[];
|
|
553
553
|
/** Enable tiered auto-apply: low-risk changes apply without approval. Default: false. */
|
|
554
554
|
autoApply?: boolean;
|
|
555
555
|
/** Target a specific agent slug (for per-agent improvement cycles). */
|