clementine-agent 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/execution-advisor.d.ts +8 -0
- package/dist/agent/execution-advisor.js +78 -52
- package/dist/cli/dashboard.js +40 -1
- package/dist/cli/index.js +462 -3
- package/dist/config/clementine-json.d.ts +7 -0
- package/dist/config/clementine-json.js +28 -0
- package/dist/config/config-doctor.d.ts +38 -0
- package/dist/config/config-doctor.js +270 -0
- package/dist/config/effective-config.d.ts +40 -0
- package/dist/config/effective-config.js +215 -0
- package/dist/config/keychain-fix-acl.d.ts +56 -0
- package/dist/config/keychain-fix-acl.js +93 -0
- package/dist/config/migrate-from-keychain.d.ts +70 -0
- package/dist/config/migrate-from-keychain.js +173 -0
- package/dist/config/migrate-keychain.d.ts +55 -0
- package/dist/config/migrate-keychain.js +144 -0
- package/dist/config.d.ts +5 -3
- package/dist/config.js +99 -35
- package/dist/gateway/cron-scheduler.d.ts +19 -0
- package/dist/gateway/cron-scheduler.js +51 -3
- package/dist/secrets/keychain.js +8 -2
- package/dist/secrets/sensitivity.d.ts +18 -0
- package/dist/secrets/sensitivity.js +43 -0
- package/dist/tools/admin-tools.js +8 -3
- package/dist/vault-migrations/0005-create-clementine-json.js +24 -0
- package/package.json +1 -1
|
@@ -24,7 +24,15 @@ export interface ReflectionEntry {
|
|
|
24
24
|
gap: string;
|
|
25
25
|
commNote: string;
|
|
26
26
|
}
|
|
27
|
+
export type AdvisorRulesMode = 'off' | 'shadow' | 'primary';
|
|
27
28
|
export declare function getExecutionAdvice(jobName: string, job: CronJobDefinition): ExecutionAdvice;
|
|
29
|
+
/**
|
|
30
|
+
* Mode-parameterized variant of getExecutionAdvice. Public so tests can
|
|
31
|
+
* exercise primary mode without mutating module-level env state.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getExecutionAdviceWithMode(jobName: string, job: CronJobDefinition, mode: AdvisorRulesMode): ExecutionAdvice;
|
|
34
|
+
/** Test-only: clear the rule-init flag so subsequent calls re-init. */
|
|
35
|
+
export declare function _resetAdvisorRulesInit(): void;
|
|
28
36
|
export declare function checkTurnLimitHits(runs: ReturnType<CronRunLog['readRecent']>, job: CronJobDefinition, advice: ExecutionAdvice): void;
|
|
29
37
|
export declare function checkReflectionQuality(reflections: ReflectionEntry[], job: CronJobDefinition, advice: ExecutionAdvice): void;
|
|
30
38
|
export declare function checkTimeoutHits(runs: ReturnType<CronRunLog['readRecent']>, job: CronJobDefinition, advice: ExecutionAdvice): void;
|
|
@@ -11,8 +11,12 @@ import pino from 'pino';
|
|
|
11
11
|
import { ADVISOR_RULES_LOADER, CRON_REFLECTIONS_DIR, ADVISOR_LOG_PATH } from '../config.js';
|
|
12
12
|
import { CronRunLog } from '../gateway/heartbeat.js';
|
|
13
13
|
import { evolvePrompt } from './prompt-evolver.js';
|
|
14
|
+
import { loadAdvisorRules, getLoadedRules, watchUserRulesDir, } from './advisor-rules/loader.js';
|
|
15
|
+
import { buildRuleContext } from './advisor-rules/context.js';
|
|
16
|
+
import { applyRules } from './advisor-rules/engine.js';
|
|
14
17
|
const logger = pino({ name: 'clementine.execution-advisor' });
|
|
15
18
|
const shadowLogger = pino({ name: 'clementine.advisor-rules-shadow' });
|
|
19
|
+
const primaryLogger = pino({ name: 'clementine.advisor-rules-primary' });
|
|
16
20
|
// ── Tier caps for maxTurns ──────────────────────────────────────────
|
|
17
21
|
export const TIER_MAX_TURNS = {
|
|
18
22
|
1: 15,
|
|
@@ -22,8 +26,32 @@ export const DEFAULT_TIMEOUT_MS = 600_000; // 10 minutes
|
|
|
22
26
|
export const MAX_TIMEOUT_MS = 20 * 60 * 1000; // 20 minutes
|
|
23
27
|
export const CIRCUIT_BREAKER_COOLDOWN_MS = 60 * 60 * 1000; // 1 hour between retry probes
|
|
24
28
|
export const DEFAULT_MAX_TURNS_FALLBACK = 5; // when job.maxTurns is unset
|
|
25
|
-
// ── Core function ───────────────────────────────────────────────────
|
|
26
29
|
export function getExecutionAdvice(jobName, job) {
|
|
30
|
+
return getExecutionAdviceWithMode(jobName, job, ADVISOR_RULES_LOADER);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Mode-parameterized variant of getExecutionAdvice. Public so tests can
|
|
34
|
+
* exercise primary mode without mutating module-level env state.
|
|
35
|
+
*/
|
|
36
|
+
export function getExecutionAdviceWithMode(jobName, job, mode) {
|
|
37
|
+
// Primary mode: rule engine is the source of truth. Falls through to the
|
|
38
|
+
// legacy TS path only if the loader is unavailable for some reason.
|
|
39
|
+
if (mode === 'primary') {
|
|
40
|
+
if (ensureRulesInitialized()) {
|
|
41
|
+
return computePrimaryAdvice(jobName, job);
|
|
42
|
+
}
|
|
43
|
+
primaryLogger.warn({ jobName }, 'Primary rule engine unavailable — falling back to legacy TS path');
|
|
44
|
+
}
|
|
45
|
+
const advice = computeLegacyAdvice(jobName, job);
|
|
46
|
+
// Shadow mode: run the YAML rule engine on the same job, log any divergence
|
|
47
|
+
// from the legacy TS advice. Non-throwing — never affects the returned advice.
|
|
48
|
+
if (mode === 'shadow' && ensureRulesInitialized()) {
|
|
49
|
+
runShadowComparison(jobName, job, advice);
|
|
50
|
+
}
|
|
51
|
+
return advice;
|
|
52
|
+
}
|
|
53
|
+
// ── Legacy TS path (kept as fallback for primary mode) ──────────────
|
|
54
|
+
function computeLegacyAdvice(jobName, job) {
|
|
27
55
|
const advice = {
|
|
28
56
|
adjustedMaxTurns: null,
|
|
29
57
|
adjustedModel: null,
|
|
@@ -101,67 +129,65 @@ export function getExecutionAdvice(jobName, job) {
|
|
|
101
129
|
catch (err) {
|
|
102
130
|
logger.warn({ err, job: jobName }, 'Execution advisor error — proceeding with defaults');
|
|
103
131
|
}
|
|
104
|
-
// Shadow mode: run the YAML rule engine on the same job, log any divergence
|
|
105
|
-
// from the legacy TS advice. Non-throwing — never affects the returned advice.
|
|
106
|
-
if (ADVISOR_RULES_LOADER === 'shadow') {
|
|
107
|
-
runShadowComparison(jobName, job, advice);
|
|
108
|
-
}
|
|
109
132
|
return advice;
|
|
110
133
|
}
|
|
111
|
-
// ──
|
|
112
|
-
let
|
|
113
|
-
let
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
return;
|
|
118
|
-
|
|
134
|
+
// ── Rule-engine path ────────────────────────────────────────────────
|
|
135
|
+
let rulesInitialized = false;
|
|
136
|
+
let rulesAvailable = false;
|
|
137
|
+
/** Sync init for the rule loader. Idempotent. Safe to call from any mode. */
|
|
138
|
+
function ensureRulesInitialized() {
|
|
139
|
+
if (rulesInitialized)
|
|
140
|
+
return rulesAvailable;
|
|
141
|
+
rulesInitialized = true;
|
|
119
142
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
loadAdvisorRules();
|
|
144
|
+
watchUserRulesDir();
|
|
145
|
+
rulesAvailable = true;
|
|
146
|
+
primaryLogger.info({ ruleCount: getLoadedRules().length }, 'Advisor rules initialized');
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
primaryLogger.warn({ err }, 'Failed to initialize advisor rules — TS path will be used');
|
|
150
|
+
rulesAvailable = false;
|
|
151
|
+
}
|
|
152
|
+
return rulesAvailable;
|
|
153
|
+
}
|
|
154
|
+
function computePrimaryAdvice(jobName, job) {
|
|
155
|
+
try {
|
|
156
|
+
const rules = getLoadedRules();
|
|
157
|
+
const ctx = buildRuleContext(jobName, job);
|
|
158
|
+
const { advice, traces } = applyRules(rules, ctx);
|
|
159
|
+
const fired = traces.filter(t => t.fired).map(t => t.ruleId);
|
|
160
|
+
if (fired.length > 0) {
|
|
161
|
+
primaryLogger.debug({ jobName, firedRules: fired }, 'Rule engine produced advice');
|
|
162
|
+
}
|
|
163
|
+
return advice;
|
|
136
164
|
}
|
|
137
165
|
catch (err) {
|
|
138
|
-
|
|
166
|
+
primaryLogger.warn({ err, jobName }, 'Rule engine threw — falling back to legacy TS path for this call');
|
|
167
|
+
return computeLegacyAdvice(jobName, job);
|
|
139
168
|
}
|
|
140
169
|
}
|
|
141
170
|
function runShadowComparison(jobName, job, tsAdvice) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const rules = shadowDeps.getLoadedRules();
|
|
150
|
-
const ctx = shadowDeps.buildRuleContext(jobName, job);
|
|
151
|
-
const { advice: yamlAdvice, traces } = shadowDeps.applyRules(rules, ctx);
|
|
152
|
-
const diffs = diffAdvice(tsAdvice, yamlAdvice);
|
|
153
|
-
if (diffs.length > 0) {
|
|
154
|
-
shadowLogger.warn({ jobName, diffs, firedRules: traces.filter(t => t.fired).map(t => t.ruleId) }, 'Shadow advisor diverged from TS path');
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
shadowLogger.debug({ jobName, firedRules: traces.filter(t => t.fired).map(t => t.ruleId) }, 'Shadow advisor matches TS path');
|
|
158
|
-
}
|
|
171
|
+
try {
|
|
172
|
+
const rules = getLoadedRules();
|
|
173
|
+
const ctx = buildRuleContext(jobName, job);
|
|
174
|
+
const { advice: yamlAdvice, traces } = applyRules(rules, ctx);
|
|
175
|
+
const diffs = diffAdvice(tsAdvice, yamlAdvice);
|
|
176
|
+
if (diffs.length > 0) {
|
|
177
|
+
shadowLogger.warn({ jobName, diffs, firedRules: traces.filter(t => t.fired).map(t => t.ruleId) }, 'Shadow advisor diverged from TS path');
|
|
159
178
|
}
|
|
160
|
-
|
|
161
|
-
shadowLogger.
|
|
179
|
+
else {
|
|
180
|
+
shadowLogger.debug({ jobName, firedRules: traces.filter(t => t.fired).map(t => t.ruleId) }, 'Shadow advisor matches TS path');
|
|
162
181
|
}
|
|
163
|
-
}
|
|
164
|
-
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
shadowLogger.warn({ err, jobName }, 'Shadow advisor run failed');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Test-only: clear the rule-init flag so subsequent calls re-init. */
|
|
188
|
+
export function _resetAdvisorRulesInit() {
|
|
189
|
+
rulesInitialized = false;
|
|
190
|
+
rulesAvailable = false;
|
|
165
191
|
}
|
|
166
192
|
function diffAdvice(a, b) {
|
|
167
193
|
const fields = [
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -7124,6 +7124,23 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
7124
7124
|
res.json({ byType: {}, totalOutcomes: 0 });
|
|
7125
7125
|
}
|
|
7126
7126
|
});
|
|
7127
|
+
app.get('/api/advisor/status', async (_req, res) => {
|
|
7128
|
+
try {
|
|
7129
|
+
const { ADVISOR_RULES_LOADER } = await import('../config.js');
|
|
7130
|
+
const { loadAdvisorRules } = await import('../agent/advisor-rules/loader.js');
|
|
7131
|
+
const rules = loadAdvisorRules();
|
|
7132
|
+
const builtinCount = rules.filter(r => r._sourcePath?.includes('/builtin/')).length;
|
|
7133
|
+
res.json({
|
|
7134
|
+
mode: ADVISOR_RULES_LOADER,
|
|
7135
|
+
ruleCount: rules.length,
|
|
7136
|
+
builtinCount,
|
|
7137
|
+
userCount: rules.length - builtinCount,
|
|
7138
|
+
});
|
|
7139
|
+
}
|
|
7140
|
+
catch (err) {
|
|
7141
|
+
res.json({ mode: 'off', ruleCount: 0, builtinCount: 0, userCount: 0, error: String(err) });
|
|
7142
|
+
}
|
|
7143
|
+
});
|
|
7127
7144
|
// ── Remote access API ────────────────────────────────────────────
|
|
7128
7145
|
app.get('/api/remote-access', (_req, res) => {
|
|
7129
7146
|
const config = loadRemoteConfig();
|
|
@@ -19927,12 +19944,34 @@ async function refreshHomeSessions() {
|
|
|
19927
19944
|
// ── Execution Analytics ───────────────────
|
|
19928
19945
|
async function refreshAdvisorAnalytics() {
|
|
19929
19946
|
try {
|
|
19930
|
-
const r = await
|
|
19947
|
+
const [statusR, r] = await Promise.all([
|
|
19948
|
+
apiFetch('/api/advisor/status'),
|
|
19949
|
+
apiFetch('/api/advisor/analytics'),
|
|
19950
|
+
]);
|
|
19951
|
+
const status = await statusR.json();
|
|
19931
19952
|
const data = await r.json();
|
|
19932
19953
|
const container = document.getElementById('advisor-analytics-content');
|
|
19933
19954
|
|
|
19934
19955
|
let html = '';
|
|
19935
19956
|
|
|
19957
|
+
// Mode chip — surfaces the active rule-engine mode at a glance.
|
|
19958
|
+
const modeColors = { off: 'var(--gray)', shadow: 'var(--blue)', primary: 'var(--green)' };
|
|
19959
|
+
const modeColor = modeColors[status.mode] || 'var(--gray)';
|
|
19960
|
+
const modeHints = {
|
|
19961
|
+
off: 'Legacy TS path is the source of truth.',
|
|
19962
|
+
shadow: 'Rule engine runs alongside the TS path; divergences are logged.',
|
|
19963
|
+
primary: 'Rule engine is the source of truth.',
|
|
19964
|
+
};
|
|
19965
|
+
const modeHint = modeHints[status.mode] || '';
|
|
19966
|
+
html += '<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;padding:10px 14px;background:var(--bg-elev);border-radius:8px;border-left:3px solid ' + modeColor + '">';
|
|
19967
|
+
html += '<span style="font-size:11px;text-transform:uppercase;letter-spacing:0.05em;color:var(--text-dim)">Advisor mode</span>';
|
|
19968
|
+
html += '<span style="font-weight:600;color:' + modeColor + '">' + esc(status.mode) + '</span>';
|
|
19969
|
+
html += '<span style="font-size:12px;color:var(--text-dim)">' + esc(modeHint) + '</span>';
|
|
19970
|
+
html += '<span style="margin-left:auto;font-size:11px;color:var(--text-dim)">' + status.ruleCount + ' rule' + (status.ruleCount === 1 ? '' : 's') + ' loaded';
|
|
19971
|
+
if (status.userCount > 0) html += ' (' + status.userCount + ' user)';
|
|
19972
|
+
html += '</span>';
|
|
19973
|
+
html += '</div>';
|
|
19974
|
+
|
|
19936
19975
|
// Summary cards row
|
|
19937
19976
|
html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-bottom:20px">';
|
|
19938
19977
|
const stats = [
|