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.
@@ -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 ───────────────────────────────────────────────
@@ -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
- import { todayISO, CronRunLog } from '../gateway/cron-scheduler.js';
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
- let jobs = [];
1274
- // Load main CRON.md
1275
- if (existsSync(CRON_FILE)) {
1276
- try {
1277
- const raw = readFileSync(CRON_FILE, 'utf-8');
1278
- const parsed = matter(raw);
1279
- const mainJobs = (parsed.data.jobs ?? []);
1280
- jobs.push(...mainJobs);
1281
- }
1282
- catch { /* ignore */ }
1283
- }
1284
- // Load agent-scoped CRON.md files
1285
- const agentsDir = path.join(VAULT_DIR, '00-System', 'agents');
1286
- if (existsSync(agentsDir)) {
1287
- try {
1288
- for (const slug of readdirSync(agentsDir)) {
1289
- const agentCronFile = path.join(agentsDir, slug, 'CRON.md');
1290
- if (!existsSync(agentCronFile))
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 = jobs.map((job) => {
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
- const result = await gw.handleSelfImprove('deny', { experimentId: req.params.id });
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
- legacyCount = (visibleTasks || []).filter(function(t) {
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
- if (legacyCount > 0 && !dismissed) {
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)">' + legacyCount + ' legacy cron task' + (legacyCount === 1 ? '' : 's') + ' can become scheduled skills</div>'
27364
- + '<div style="font-size:11px;color:var(--text-muted);margin-top:2px">Skills are the Anthropic-pure unit of work. Scheduled-skill format is thinner, easier to maintain, and tasks can be reused on demand.</div>'
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
- const r = await apiFetch('/api/self-improve/deny/' + id, { method: 'POST' });
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');
@@ -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). */
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.159",
3
+ "version": "1.18.161",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",