clementine-agent 1.18.159 → 1.18.161
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/approval-signals.d.ts +58 -0
- package/dist/agent/approval-signals.js +105 -0
- package/dist/agent/self-improve.d.ts +1 -1
- package/dist/agent/self-improve.js +37 -1
- package/dist/cli/dashboard.js +61 -40
- package/dist/gateway/router.d.ts +1 -0
- package/dist/gateway/router.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner-approval feedback loop for self-improve proposals (1.18.161).
|
|
3
|
+
*
|
|
4
|
+
* Background: the self-improve hypothesizer generates 1-3 proposals each
|
|
5
|
+
* cycle. The owner approves or denies each one in the dashboard. Today
|
|
6
|
+
* that decision is recorded only as a status change on the experiment row
|
|
7
|
+
* — the *implicit signal* ("this kind of fix is good / bad") is lost.
|
|
8
|
+
*
|
|
9
|
+
* This module captures the signal as an append-only JSONL log
|
|
10
|
+
* (`~/.clementine/self-improve/approval-signals.jsonl`) and exposes
|
|
11
|
+
* `formatForHypothesizer()` so the next cycle's prompt includes:
|
|
12
|
+
*
|
|
13
|
+
* ## Owner approval signals (recent)
|
|
14
|
+
* APPROVED (do more like this):
|
|
15
|
+
* - cron/insight-check: "Apply lean mode to reduce prompt size"
|
|
16
|
+
* - agent/sasha-the-cmo: "Add explicit citation requirement to system prompt"
|
|
17
|
+
*
|
|
18
|
+
* DENIED (avoid these patterns):
|
|
19
|
+
* - workflow/email-gen: "Replace template with LLM generation" ← user note: "too generic; loses voice"
|
|
20
|
+
*
|
|
21
|
+
* The hypothesizer reads this and biases future proposals — favoring
|
|
22
|
+
* patterns the owner has approved, avoiding patterns they've denied.
|
|
23
|
+
*
|
|
24
|
+
* Closed-loop autonomy: the system learns from human feedback without
|
|
25
|
+
* needing the human to write rules. Just react to proposals as usual.
|
|
26
|
+
*/
|
|
27
|
+
export interface ApprovalSignal {
|
|
28
|
+
/** ISO timestamp of the decision. */
|
|
29
|
+
ts: string;
|
|
30
|
+
/** Self-improve experiment ID this decision applies to. */
|
|
31
|
+
experimentId: string;
|
|
32
|
+
/** The area the proposal targeted (cron, agent, skill, soul, etc.). */
|
|
33
|
+
area: string;
|
|
34
|
+
/** The specific target (e.g., "insight-check", "sasha-the-cmo"). */
|
|
35
|
+
target: string;
|
|
36
|
+
/** The proposal's one-sentence hypothesis (truncated to 200 chars). */
|
|
37
|
+
hypothesis: string;
|
|
38
|
+
/** Owner's decision. */
|
|
39
|
+
decision: 'approved' | 'denied';
|
|
40
|
+
/** Optional free-text note from the owner explaining the decision. */
|
|
41
|
+
noteFromOwner?: string;
|
|
42
|
+
}
|
|
43
|
+
/** Append a new signal to the log. Best-effort — never throws to the caller. */
|
|
44
|
+
export declare function recordApprovalSignal(signal: Omit<ApprovalSignal, 'ts'>): void;
|
|
45
|
+
/**
|
|
46
|
+
* Read the most recent N signals from the log. Returns newest-first.
|
|
47
|
+
* Defaults to 50 — enough for the hypothesizer to see patterns, not so
|
|
48
|
+
* many that we bloat its prompt.
|
|
49
|
+
*/
|
|
50
|
+
export declare function getRecentApprovalSignals(limit?: number): ApprovalSignal[];
|
|
51
|
+
/**
|
|
52
|
+
* Render a recent-signals prompt block for the hypothesizer. Returns the
|
|
53
|
+
* empty string when there are no signals (so the prompt stays clean for
|
|
54
|
+
* fresh installs). Caps at the most recent 8 of each kind to keep the
|
|
55
|
+
* block compact.
|
|
56
|
+
*/
|
|
57
|
+
export declare function formatApprovalSignalsForHypothesizer(): string;
|
|
58
|
+
//# sourceMappingURL=approval-signals.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner-approval feedback loop for self-improve proposals (1.18.161).
|
|
3
|
+
*
|
|
4
|
+
* Background: the self-improve hypothesizer generates 1-3 proposals each
|
|
5
|
+
* cycle. The owner approves or denies each one in the dashboard. Today
|
|
6
|
+
* that decision is recorded only as a status change on the experiment row
|
|
7
|
+
* — the *implicit signal* ("this kind of fix is good / bad") is lost.
|
|
8
|
+
*
|
|
9
|
+
* This module captures the signal as an append-only JSONL log
|
|
10
|
+
* (`~/.clementine/self-improve/approval-signals.jsonl`) and exposes
|
|
11
|
+
* `formatForHypothesizer()` so the next cycle's prompt includes:
|
|
12
|
+
*
|
|
13
|
+
* ## Owner approval signals (recent)
|
|
14
|
+
* APPROVED (do more like this):
|
|
15
|
+
* - cron/insight-check: "Apply lean mode to reduce prompt size"
|
|
16
|
+
* - agent/sasha-the-cmo: "Add explicit citation requirement to system prompt"
|
|
17
|
+
*
|
|
18
|
+
* DENIED (avoid these patterns):
|
|
19
|
+
* - workflow/email-gen: "Replace template with LLM generation" ← user note: "too generic; loses voice"
|
|
20
|
+
*
|
|
21
|
+
* The hypothesizer reads this and biases future proposals — favoring
|
|
22
|
+
* patterns the owner has approved, avoiding patterns they've denied.
|
|
23
|
+
*
|
|
24
|
+
* Closed-loop autonomy: the system learns from human feedback without
|
|
25
|
+
* needing the human to write rules. Just react to proposals as usual.
|
|
26
|
+
*/
|
|
27
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import { BASE_DIR } from '../config.js';
|
|
30
|
+
/** Where the append-only signals log lives. */
|
|
31
|
+
function signalsLogPath() {
|
|
32
|
+
return path.join(BASE_DIR, 'self-improve', 'approval-signals.jsonl');
|
|
33
|
+
}
|
|
34
|
+
/** Append a new signal to the log. Best-effort — never throws to the caller. */
|
|
35
|
+
export function recordApprovalSignal(signal) {
|
|
36
|
+
try {
|
|
37
|
+
const file = signalsLogPath();
|
|
38
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
39
|
+
const entry = {
|
|
40
|
+
ts: new Date().toISOString(),
|
|
41
|
+
...signal,
|
|
42
|
+
// Truncate hypothesis to keep the log compact + searchable.
|
|
43
|
+
hypothesis: (signal.hypothesis || '').slice(0, 200),
|
|
44
|
+
};
|
|
45
|
+
appendFileSync(file, JSON.stringify(entry) + '\n');
|
|
46
|
+
}
|
|
47
|
+
catch { /* never block the apply/deny path on telemetry */ }
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Read the most recent N signals from the log. Returns newest-first.
|
|
51
|
+
* Defaults to 50 — enough for the hypothesizer to see patterns, not so
|
|
52
|
+
* many that we bloat its prompt.
|
|
53
|
+
*/
|
|
54
|
+
export function getRecentApprovalSignals(limit = 50) {
|
|
55
|
+
const file = signalsLogPath();
|
|
56
|
+
if (!existsSync(file))
|
|
57
|
+
return [];
|
|
58
|
+
try {
|
|
59
|
+
const lines = readFileSync(file, 'utf-8').trim().split('\n').filter(Boolean);
|
|
60
|
+
const recent = [];
|
|
61
|
+
for (let i = lines.length - 1; i >= 0 && recent.length < limit; i--) {
|
|
62
|
+
try {
|
|
63
|
+
recent.push(JSON.parse(lines[i]));
|
|
64
|
+
}
|
|
65
|
+
catch { /* skip malformed lines */ }
|
|
66
|
+
}
|
|
67
|
+
return recent;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Render a recent-signals prompt block for the hypothesizer. Returns the
|
|
75
|
+
* empty string when there are no signals (so the prompt stays clean for
|
|
76
|
+
* fresh installs). Caps at the most recent 8 of each kind to keep the
|
|
77
|
+
* block compact.
|
|
78
|
+
*/
|
|
79
|
+
export function formatApprovalSignalsForHypothesizer() {
|
|
80
|
+
const signals = getRecentApprovalSignals(40);
|
|
81
|
+
if (signals.length === 0)
|
|
82
|
+
return '';
|
|
83
|
+
const approved = signals.filter(s => s.decision === 'approved').slice(0, 8);
|
|
84
|
+
const denied = signals.filter(s => s.decision === 'denied').slice(0, 8);
|
|
85
|
+
if (approved.length === 0 && denied.length === 0)
|
|
86
|
+
return '';
|
|
87
|
+
const fmt = (s) => {
|
|
88
|
+
const note = s.noteFromOwner ? ` ← owner note: "${s.noteFromOwner.slice(0, 120)}"` : '';
|
|
89
|
+
return `- ${s.area}/${s.target}: "${s.hypothesis}"${note}`;
|
|
90
|
+
};
|
|
91
|
+
const parts = ['### Owner approval signals (recent)'];
|
|
92
|
+
if (approved.length > 0) {
|
|
93
|
+
parts.push('APPROVED (do more like these):');
|
|
94
|
+
parts.push(approved.map(fmt).join('\n'));
|
|
95
|
+
}
|
|
96
|
+
if (denied.length > 0) {
|
|
97
|
+
parts.push('DENIED (avoid these patterns):');
|
|
98
|
+
parts.push(denied.map(fmt).join('\n'));
|
|
99
|
+
}
|
|
100
|
+
parts.push('Bias today\'s proposals toward the approved patterns and away from the denied ones. ' +
|
|
101
|
+
'If a denied pattern reflects a misunderstanding (e.g. you proposed the wrong target), ' +
|
|
102
|
+
'reframe — don\'t just avoid the area entirely.');
|
|
103
|
+
return parts.join('\n') + '\n\n';
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=approval-signals.js.map
|
|
@@ -58,7 +58,7 @@ export declare class SelfImproveLoop {
|
|
|
58
58
|
private savePendingChange;
|
|
59
59
|
applyApprovedChange(experimentId: string): Promise<string>;
|
|
60
60
|
/** Deny a pending change without applying it. */
|
|
61
|
-
denyChange(experimentId: string): string;
|
|
61
|
+
denyChange(experimentId: string, noteFromOwner?: string): string;
|
|
62
62
|
private runMemoryCleanup;
|
|
63
63
|
private synthesizeFeedbackPatterns;
|
|
64
64
|
/** Update the structured user model from interaction data. */
|
|
@@ -18,6 +18,7 @@ import { BASE_DIR, SELF_IMPROVE_DIR, SOUL_FILE, CRON_FILE, WORKFLOWS_DIR, VAULT_
|
|
|
18
18
|
import { listAllGoals } from '../tools/shared.js';
|
|
19
19
|
import { MemoryStore } from '../memory/store.js';
|
|
20
20
|
import { ANTHROPIC_SKILL_NAME_PATTERN } from './skill-store.js';
|
|
21
|
+
import { recordApprovalSignal, formatApprovalSignalsForHypothesizer } from './approval-signals.js';
|
|
21
22
|
const logger = pino({ name: 'clementine.self-improve' });
|
|
22
23
|
// ── Defaults ─────────────────────────────────────────────────────────
|
|
23
24
|
const DEFAULT_CONFIG = {
|
|
@@ -1097,6 +1098,10 @@ export class SelfImproveLoop {
|
|
|
1097
1098
|
}
|
|
1098
1099
|
}
|
|
1099
1100
|
catch { /* non-fatal */ }
|
|
1101
|
+
// Owner-approval feedback (1.18.161) — bias hypotheses toward patterns the
|
|
1102
|
+
// owner has approved, away from those they've denied. Empty string for
|
|
1103
|
+
// fresh installs, which keeps the prompt clean.
|
|
1104
|
+
const approvalSignalsText = formatApprovalSignalsForHypothesizer();
|
|
1100
1105
|
// ── Step 1: Analysis — identify top opportunities from metrics (no config dumps) ──
|
|
1101
1106
|
const analysisPrompt = `You are Clementine's self-improvement strategist. Analyze the performance data below and identify the top 3 improvement opportunities.\n\n` +
|
|
1102
1107
|
`## Recent Performance Data (last 7 days)\n` +
|
|
@@ -1114,6 +1119,7 @@ export class SelfImproveLoop {
|
|
|
1114
1119
|
diversityConstraint +
|
|
1115
1120
|
agentFocusText +
|
|
1116
1121
|
soulCandidatesText +
|
|
1122
|
+
(approvalSignalsText ? `\n${approvalSignalsText}` : '') +
|
|
1117
1123
|
`\n## Instructions\n` +
|
|
1118
1124
|
`Propose **1-3 concrete, high-impact improvements** the owner should review today — no fewer (aim for at least one actionable suggestion when data warrants it), no more (the owner reads each proposal manually and you'll overwhelm them). Rank by expected impact; drop anything below "solid idea".\n\n` +
|
|
1119
1125
|
`For each opportunity, specify:\n` +
|
|
@@ -1486,14 +1492,33 @@ export class SelfImproveLoop {
|
|
|
1486
1492
|
catch (err) {
|
|
1487
1493
|
logger.warn({ err }, 'Failed to schedule impact check');
|
|
1488
1494
|
}
|
|
1495
|
+
// 1.18.161 — record the implicit owner-approval signal so future
|
|
1496
|
+
// hypothesizer cycles can see "the owner approved fixes like this"
|
|
1497
|
+
// and bias proposals accordingly. Best-effort, never blocks apply.
|
|
1498
|
+
recordApprovalSignal({
|
|
1499
|
+
experimentId,
|
|
1500
|
+
area: pending.area,
|
|
1501
|
+
target: pending.target,
|
|
1502
|
+
hypothesis: pending.hypothesis,
|
|
1503
|
+
decision: 'approved',
|
|
1504
|
+
});
|
|
1489
1505
|
return `Applied change to ${pending.area}/${pending.target}`;
|
|
1490
1506
|
}
|
|
1491
1507
|
/** Deny a pending change without applying it. */
|
|
1492
|
-
denyChange(experimentId) {
|
|
1508
|
+
denyChange(experimentId, noteFromOwner) {
|
|
1493
1509
|
const pendingFile = path.join(PENDING_DIR, `${experimentId}.json`);
|
|
1494
1510
|
if (!existsSync(pendingFile)) {
|
|
1495
1511
|
return `Pending change not found: ${experimentId}`;
|
|
1496
1512
|
}
|
|
1513
|
+
// 1.18.161 — capture the area/target/hypothesis BEFORE we delete the
|
|
1514
|
+
// pending file so the approval-signal log gets a meaningful entry
|
|
1515
|
+
// (not just an experiment ID with no context).
|
|
1516
|
+
let signalContext = null;
|
|
1517
|
+
try {
|
|
1518
|
+
const pending = JSON.parse(readFileSync(pendingFile, 'utf-8'));
|
|
1519
|
+
signalContext = { area: pending.area, target: pending.target, hypothesis: pending.hypothesis };
|
|
1520
|
+
}
|
|
1521
|
+
catch { /* file may be malformed; record a minimal signal below */ }
|
|
1497
1522
|
this.updateExperimentStatus(experimentId, 'denied');
|
|
1498
1523
|
try {
|
|
1499
1524
|
unlinkSync(pendingFile);
|
|
@@ -1502,6 +1527,17 @@ export class SelfImproveLoop {
|
|
|
1502
1527
|
const state = this.loadState();
|
|
1503
1528
|
state.pendingApprovals = Math.max(0, state.pendingApprovals - 1);
|
|
1504
1529
|
this.saveState(state);
|
|
1530
|
+
// 1.18.161 — record the denial signal. Owner can pass an optional note
|
|
1531
|
+
// (via the dashboard Reason field, or via Discord) explaining why so
|
|
1532
|
+
// the hypothesizer learns more than just "no."
|
|
1533
|
+
recordApprovalSignal({
|
|
1534
|
+
experimentId,
|
|
1535
|
+
area: signalContext?.area ?? 'unknown',
|
|
1536
|
+
target: signalContext?.target ?? 'unknown',
|
|
1537
|
+
hypothesis: signalContext?.hypothesis ?? '(pending file unreadable at deny time)',
|
|
1538
|
+
decision: 'denied',
|
|
1539
|
+
...(noteFromOwner ? { noteFromOwner } : {}),
|
|
1540
|
+
});
|
|
1505
1541
|
return `Denied change: ${experimentId}`;
|
|
1506
1542
|
}
|
|
1507
1543
|
// ── Memory cleanup ───────────────────────────────────────────────
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -21,7 +21,11 @@ import { discoverMcpServers, getClaudeIntegrations, KNOWN_MCP_DESCRIPTIONS } fro
|
|
|
21
21
|
import { buildBuilderEnrichedMessage, builderSessionKey } from '../dashboard/builder/prompt.js';
|
|
22
22
|
import { AGENTS_DIR, MEMORY_FILE, SESSIONS_FILE, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
|
|
23
23
|
import { parseTasks } from '../tools/shared.js';
|
|
24
|
-
|
|
24
|
+
// 1.18.160 — also pull parseCronJobs + parseAgentCronJobs so getCronJobs()
|
|
25
|
+
// returns the same merged set the runtime fires (CRON.md + agent CRON +
|
|
26
|
+
// schedule registry). Was reading only CRON.md before, hiding migrated
|
|
27
|
+
// scheduled-skills from the Tasks tab while they kept firing on schedule.
|
|
28
|
+
import { todayISO, CronRunLog, parseCronJobs, parseAgentCronJobs } from '../gateway/cron-scheduler.js';
|
|
25
29
|
import { goalsRouter } from './routes/goals.js';
|
|
26
30
|
import { delegationsRouter } from './routes/delegations.js';
|
|
27
31
|
import { workflowsRouter } from './routes/workflows.js';
|
|
@@ -1270,43 +1274,29 @@ function getSessions() {
|
|
|
1270
1274
|
}
|
|
1271
1275
|
}
|
|
1272
1276
|
function getCronJobs() {
|
|
1273
|
-
|
|
1274
|
-
//
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
//
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
continue;
|
|
1292
|
-
try {
|
|
1293
|
-
const raw = readFileSync(agentCronFile, 'utf-8');
|
|
1294
|
-
const parsed = matter(raw);
|
|
1295
|
-
const agentJobs = (parsed.data.jobs ?? []);
|
|
1296
|
-
for (const job of agentJobs) {
|
|
1297
|
-
jobs.push({ ...job, agent: slug, name: `${slug}:${job.name}` });
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
catch { /* ignore individual agent parse errors */ }
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
catch { /* ignore */ }
|
|
1304
|
-
}
|
|
1277
|
+
// 1.18.160 — delegate to the canonical merger so scheduled-skill rows
|
|
1278
|
+
// (from ~/.clementine/schedules.json) reach the dashboard alongside
|
|
1279
|
+
// legacy CRON.md entries. Before this, getCronJobs() only read CRON.md
|
|
1280
|
+
// — so when a user migrated 14 of their 15 crons to scheduled-skills,
|
|
1281
|
+
// the Tasks page silently dropped to 1 card while the runtime kept
|
|
1282
|
+
// firing all 22 jobs. The result LOOKED like a regression because the
|
|
1283
|
+
// user couldn't see, edit, pause, or trace any of the migrated work
|
|
1284
|
+
// from the Tasks tab. parseCronJobs() reads BOTH CRON.md + agent CRON
|
|
1285
|
+
// files + the schedule registry, dedups by name (scheduled-skill wins
|
|
1286
|
+
// collisions), and stamps `source` so the existing card renderer can
|
|
1287
|
+
// branch on SKILL vs LEGACY CRON badge.
|
|
1288
|
+
//
|
|
1289
|
+
// The dashboard now sees exactly what the runtime fires — single
|
|
1290
|
+
// source of truth, no drift. Both helpers are imported at the top.
|
|
1291
|
+
const allJobs = [
|
|
1292
|
+
...parseCronJobs(),
|
|
1293
|
+
...parseAgentCronJobs(path.join(VAULT_DIR, '00-System', 'agents')),
|
|
1294
|
+
];
|
|
1305
1295
|
// Attach recent run history. Single source of truth via CronRunLog.readRecent
|
|
1306
1296
|
// — same path the new /api/cron/runs cross-job endpoint uses, so per-card
|
|
1307
1297
|
// last-run and the Recent History zone never disagree.
|
|
1308
1298
|
const log = new CronRunLog();
|
|
1309
|
-
const enriched =
|
|
1299
|
+
const enriched = allJobs.map((job) => {
|
|
1310
1300
|
const name = String(job.name ?? '');
|
|
1311
1301
|
const recentRuns = log.readRecent(name, 10);
|
|
1312
1302
|
return { ...job, recentRuns };
|
|
@@ -11512,7 +11502,14 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
11512
11502
|
app.post('/api/self-improve/deny/:id', async (req, res) => {
|
|
11513
11503
|
try {
|
|
11514
11504
|
const gw = await getGateway();
|
|
11515
|
-
|
|
11505
|
+
// 1.18.161 — accept an optional `noteFromOwner` in the body so the
|
|
11506
|
+
// approval-signal log captures the *reason* for denial (the
|
|
11507
|
+
// hypothesizer learns more from "too generic — loses voice" than
|
|
11508
|
+
// from a bare "no").
|
|
11509
|
+
const noteFromOwner = typeof req.body?.noteFromOwner === 'string'
|
|
11510
|
+
? req.body.noteFromOwner.slice(0, 500)
|
|
11511
|
+
: undefined;
|
|
11512
|
+
const result = await gw.handleSelfImprove('deny', { experimentId: req.params.id, noteFromOwner });
|
|
11516
11513
|
res.json({ ok: true, message: result });
|
|
11517
11514
|
}
|
|
11518
11515
|
catch (err) {
|
|
@@ -27344,15 +27341,29 @@ async function refreshCron() {
|
|
|
27344
27341
|
// (definition.source !== 'scheduled-skill') and surfaces a one-click
|
|
27345
27342
|
// bulk migrator. Dismissable; persists in localStorage so it doesn't
|
|
27346
27343
|
// nag on every refresh.
|
|
27344
|
+
// 1.18.160 — only nag for UNHEALTHY legacy crons. After the user
|
|
27345
|
+
// bulk-migrates 14 healthy ones, having a banner shout about the
|
|
27346
|
+
// 1 healthy survivor felt naggy. The user can still migrate healthy
|
|
27347
|
+
// ones at their leisure via per-row "→ Skill" buttons; the banner
|
|
27348
|
+
// earns the screen real estate only when there's a real problem.
|
|
27347
27349
|
var legacyCount = 0;
|
|
27350
|
+
var legacyUnhealthyCount = 0;
|
|
27348
27351
|
try {
|
|
27349
|
-
|
|
27352
|
+
var legacyRows = (visibleTasks || []).filter(function(t) {
|
|
27350
27353
|
return !(t.definition && t.definition.source === 'scheduled-skill');
|
|
27354
|
+
});
|
|
27355
|
+
legacyCount = legacyRows.length;
|
|
27356
|
+
legacyUnhealthyCount = legacyRows.filter(function(t) {
|
|
27357
|
+
var h = t.health;
|
|
27358
|
+
return h === 'failed' || h === 'broken' || h === 'never_run';
|
|
27351
27359
|
}).length;
|
|
27352
27360
|
} catch (_) { /* defensive */ }
|
|
27353
27361
|
var bannerHtml = '';
|
|
27354
27362
|
var dismissed = localStorage.getItem('clem-skill-migrate-banner-dismissed') === '1';
|
|
27355
|
-
|
|
27363
|
+
// Show the banner only when there's an unhealthy legacy cron to nudge
|
|
27364
|
+
// the user about. Healthy legacy crons live quietly until the user
|
|
27365
|
+
// chooses to migrate them per-row.
|
|
27366
|
+
if (legacyUnhealthyCount > 0 && !dismissed) {
|
|
27356
27367
|
// 1.18.155 — data-banner-kind tags this as the legacy-cron soft-
|
|
27357
27368
|
// deprecation banner so refreshCronMigrateBanner can suppress its
|
|
27358
27369
|
// secondary "clean up preambles" banner when this one is showing
|
|
@@ -27360,8 +27371,8 @@ async function refreshCron() {
|
|
|
27360
27371
|
bannerHtml = '<div data-banner-kind="legacy-cron-soft-deprecation" style="background:rgba(124,58,237,0.08);border:1px solid var(--purple);border-radius:8px;padding:12px 14px;margin-bottom:14px;display:flex;align-items:center;gap:12px;flex-wrap:wrap">'
|
|
27361
27372
|
+ '<span style="font-size:18px">⚡</span>'
|
|
27362
27373
|
+ '<div style="flex:1;min-width:200px">'
|
|
27363
|
-
+ '<div style="font-size:13px;font-weight:500;color:var(--text-primary)">' +
|
|
27364
|
-
+ '<div style="font-size:11px;color:var(--text-muted);margin-top:2px">
|
|
27374
|
+
+ '<div style="font-size:13px;font-weight:500;color:var(--text-primary)">' + legacyUnhealthyCount + ' legacy cron task' + (legacyUnhealthyCount === 1 ? '' : 's') + ' failing — migrating to a scheduled skill often clears the issue</div>'
|
|
27375
|
+
+ '<div style="font-size:11px;color:var(--text-muted);margin-top:2px">Scheduled skills use a tighter context envelope (lean mode for meta-jobs) and the Anthropic-canonical SKILL.md format. Healthy legacy crons stay quietly out of the way.</div>'
|
|
27365
27376
|
+ '</div>'
|
|
27366
27377
|
+ '<button class="btn-sm btn-primary" onclick="migrateAllCronsToSkills()" style="font-size:12px;padding:6px 12px">Migrate all eligible →</button>'
|
|
27367
27378
|
+ '<button class="btn-sm" onclick="localStorage.setItem(\\x27clem-skill-migrate-banner-dismissed\\x27,\\x271\\x27);refreshCron()" title="Hide this banner" style="font-size:12px;padding:6px 10px">Dismiss</button>'
|
|
@@ -40683,7 +40694,17 @@ async function siApply(id) {
|
|
|
40683
40694
|
|
|
40684
40695
|
async function siDeny(id) {
|
|
40685
40696
|
try {
|
|
40686
|
-
|
|
40697
|
+
// 1.18.161 — invite an optional one-line reason. Cancel = bare deny;
|
|
40698
|
+
// empty string = bare deny; non-empty = sent to the hypothesizer's
|
|
40699
|
+
// approval-signal log so future cycles avoid the rejected pattern.
|
|
40700
|
+
const note = window.prompt('Optional reason for denying (helps the hypothesizer learn — leave blank to skip):', '');
|
|
40701
|
+
if (note === null) return;
|
|
40702
|
+
const body = note.trim() ? JSON.stringify({ noteFromOwner: note.trim() }) : undefined;
|
|
40703
|
+
const r = await apiFetch('/api/self-improve/deny/' + id, {
|
|
40704
|
+
method: 'POST',
|
|
40705
|
+
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
|
40706
|
+
body,
|
|
40707
|
+
});
|
|
40687
40708
|
const d = await r.json();
|
|
40688
40709
|
if (d.ok) toast(d.message, 'success');
|
|
40689
40710
|
else toast(d.message || 'Failed', 'error');
|
package/dist/gateway/router.d.ts
CHANGED
|
@@ -308,6 +308,7 @@ export declare class Gateway {
|
|
|
308
308
|
getAllProvenance(): Map<string, SessionProvenance>;
|
|
309
309
|
handleSelfImprove(action: string, args?: {
|
|
310
310
|
experimentId?: string;
|
|
311
|
+
noteFromOwner?: string;
|
|
311
312
|
config?: Partial<SelfImproveConfig>;
|
|
312
313
|
}, onProposal?: (experiment: SelfImproveExperiment) => Promise<void>): Promise<string>;
|
|
313
314
|
/** Extract a procedural skill from a successful cron execution (fire-and-forget). */
|
package/dist/gateway/router.js
CHANGED
|
@@ -2437,7 +2437,7 @@ export class Gateway {
|
|
|
2437
2437
|
case 'deny': {
|
|
2438
2438
|
if (!args?.experimentId)
|
|
2439
2439
|
return 'Missing experiment ID.';
|
|
2440
|
-
return loop.denyChange(args.experimentId);
|
|
2440
|
+
return loop.denyChange(args.experimentId, args.noteFromOwner);
|
|
2441
2441
|
}
|
|
2442
2442
|
case 'run-agent': {
|
|
2443
2443
|
const slug = args?.experimentId; // Reuse experimentId field for agent slug
|