clementine-agent 1.18.184 → 1.18.185

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.
@@ -19,7 +19,7 @@ import { TunnelManager } from './tunnel.js';
19
19
  import { AgentManager } from '../agent/agent-manager.js';
20
20
  import { discoverMcpServers, getClaudeIntegrations, KNOWN_MCP_DESCRIPTIONS } from '../agent/mcp-bridge.js';
21
21
  import { buildBuilderEnrichedMessage, builderSessionKey } from '../dashboard/builder/prompt.js';
22
- import { AGENTS_DIR, MEMORY_FILE, SESSIONS_FILE, TIMEZONE, applyOneMillionContextRecovery, currentTimeZone, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, setEnvOverride, } from '../config.js';
22
+ import { AGENTS_DIR, MEMORY_FILE, MODELS, SESSIONS_FILE, TIMEZONE, applyOneMillionContextRecovery, currentTimeZone, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, setEnvOverride, } from '../config.js';
23
23
  import { parseTasks } from '../tools/shared.js';
24
24
  // 1.18.160 — also pull parseCronJobs + parseAgentCronJobs so getCronJobs()
25
25
  // returns the same merged set the runtime fires (CRON.md + agent CRON +
@@ -4655,8 +4655,20 @@ export async function cmdDashboard(opts) {
4655
4655
  app.get('/api/skills/quality', async (req, res) => {
4656
4656
  try {
4657
4657
  const { computeAllSkillQuality } = await import('../memory/skill-quality.js');
4658
+ const { listSkills } = await import('../agent/skill-store.js');
4658
4659
  const windowDays = req.query.windowDays ? Math.max(1, Math.min(365, Number(req.query.windowDays))) : undefined;
4659
- const scores = computeAllSkillQuality(windowDays ? { windowDays } : {});
4660
+ // 1.18.185 pass every vault-known skill name so freshly-created
4661
+ // skills get a 'ready' grade instead of being silently dropped
4662
+ // (or worse, rendered as 'no-data' = "this looks broken").
4663
+ let vaultSkillNames = [];
4664
+ try {
4665
+ vaultSkillNames = listSkills().map((s) => s.frontmatter?.name).filter((n) => typeof n === 'string' && n.length > 0);
4666
+ }
4667
+ catch { /* listSkills failures are non-fatal; we just lose the ready-grade enrichment */ }
4668
+ const scores = computeAllSkillQuality({
4669
+ ...(windowDays ? { windowDays } : {}),
4670
+ vaultSkillNames,
4671
+ });
4660
4672
  res.json({ ok: true, count: scores.length, scores });
4661
4673
  }
4662
4674
  catch (err) {
@@ -7917,11 +7929,50 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
7917
7929
  }
7918
7930
  });
7919
7931
  // ── CRON CRUD routes (continued) ──────────────────────────────
7932
+ /**
7933
+ * 1.18.185 — Validate a per-job model override before persisting it.
7934
+ * Without this guard, a typo in the dashboard (or CRON.md hand-edit)
7935
+ * silently writes garbage; runtime then fails with a cryptic SDK
7936
+ * error far from where the user set the value. Accepts:
7937
+ * - SDK tier aliases: 'sonnet', 'opus', 'haiku' (case-insensitive)
7938
+ * - Full model IDs from MODELS (claude-sonnet-4-6 etc.), optionally
7939
+ * with the `[1m]` Extra Usage suffix
7940
+ * - Any full claude-*-* string, leniently — keeps the door open for
7941
+ * model IDs not yet in the MODELS map (e.g., a future Opus).
7942
+ */
7943
+ function validateCronModelOverride(value) {
7944
+ const v = String(value ?? '').trim();
7945
+ if (!v)
7946
+ return { ok: false, error: 'model must be a non-empty string' };
7947
+ const lower = v.toLowerCase();
7948
+ // Tier aliases — SDK accepts these directly.
7949
+ if (lower === 'sonnet' || lower === 'opus' || lower === 'haiku')
7950
+ return { ok: true };
7951
+ // Known full IDs from MODELS (with or without [1m] suffix).
7952
+ const knownBases = new Set([MODELS.sonnet, MODELS.opus, MODELS.haiku]
7953
+ .filter((m) => typeof m === 'string' && m.length > 0)
7954
+ .map((m) => m.replace(/\[1m\]$/i, '').toLowerCase()));
7955
+ const baseOf = lower.replace(/\[1m\]$/i, '');
7956
+ if (knownBases.has(baseOf))
7957
+ return { ok: true };
7958
+ // Lenient fallback: any plausible claude-*-* string (lets users
7959
+ // adopt new model IDs without waiting for a MODELS map update).
7960
+ if (/^claude-[a-z0-9-]+(?:\[1m\])?$/i.test(v))
7961
+ return { ok: true };
7962
+ return {
7963
+ ok: false,
7964
+ error: `model "${v}" is not recognized. Use 'sonnet' / 'opus' / 'haiku', a known model ID (${Array.from(knownBases).join(', ')}), or a claude-*-* string. The [1m] suffix is allowed for explicit 1M context routing (Extra Usage on Sonnet, in-plan on Opus for Max).`,
7965
+ };
7966
+ }
7920
7967
  app.post('/api/cron', (req, res) => {
7921
7968
  try {
7922
7969
  const { name, schedule, prompt, tier, enabled, work_dir, mode, max_hours, max_retries, after, agent, context, skills, allowedTools, allowedMcpServers, tags, category, predictable,
7923
7970
  // PRD Phase 1 fields (camelCase from API; written as snake_case YAML).
7924
- successCriteriaText, successSchema, addDirs, } = req.body;
7971
+ successCriteriaText, successSchema, addDirs,
7972
+ // 1.18.185 — per-job model override, alwaysDeliver, and lean mode
7973
+ // were previously parsed from CRON.md and threaded through the
7974
+ // runtime but had no dashboard write path. Now settable here.
7975
+ model, alwaysDeliver, lean, } = req.body;
7925
7976
  if (!name || !schedule || !prompt) {
7926
7977
  res.status(400).json({ error: 'name, schedule, and prompt are required' });
7927
7978
  return;
@@ -7930,6 +7981,13 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
7930
7981
  res.status(400).json({ error: `Invalid cron expression: ${schedule}` });
7931
7982
  return;
7932
7983
  }
7984
+ if (model !== undefined && model !== null && model !== '') {
7985
+ const valid = validateCronModelOverride(model);
7986
+ if (!valid.ok) {
7987
+ res.status(400).json({ error: valid.error });
7988
+ return;
7989
+ }
7990
+ }
7933
7991
  let cronFile = CRON_FILE;
7934
7992
  if (agent) {
7935
7993
  cronFile = path.join(VAULT_DIR, '00-System', 'agents', String(agent), 'CRON.md');
@@ -7990,6 +8048,24 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
7990
8048
  if (Array.isArray(addDirs) && addDirs.length) {
7991
8049
  job.add_dirs = addDirs.map(String).map((s) => s.trim()).filter(Boolean);
7992
8050
  }
8051
+ // 1.18.185 — model override + alwaysDeliver + lean wire-through.
8052
+ // These three fields are parsed from CRON.md by the scheduler
8053
+ // (cron-scheduler.ts:194 etc.) and threaded through to runSkill /
8054
+ // handleCronJob. Without the dashboard persisting them, the only
8055
+ // way to use them was hand-editing CRON.md.
8056
+ //
8057
+ // YAML convention: snake_case (matches work_dir / max_hours /
8058
+ // allowed_tools). The parser accepts both casings defensively
8059
+ // (cron-scheduler.ts:219) but we write the canonical form.
8060
+ if (typeof model === 'string' && model.trim()) {
8061
+ job.model = model.trim();
8062
+ }
8063
+ if (alwaysDeliver === true || alwaysDeliver === 'true') {
8064
+ job.always_deliver = true;
8065
+ }
8066
+ if (lean === true || lean === 'true') {
8067
+ job.lean = true;
8068
+ }
7993
8069
  jobs.push(job);
7994
8070
  writeCronFileAt(cronFile, parsed, jobs);
7995
8071
  res.json({ ok: true, message: `Created cron job: ${name}` });
@@ -8170,6 +8246,41 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
8170
8246
  }
8171
8247
  jobs[idx].name = String(updates.name);
8172
8248
  }
8249
+ // 1.18.185 — model / alwaysDeliver / lean. set-when-non-empty,
8250
+ // delete-when-cleared, matching the rest of this endpoint's pattern.
8251
+ if (updates.model !== undefined) {
8252
+ if (typeof updates.model === 'string' && updates.model.trim()) {
8253
+ const valid = validateCronModelOverride(updates.model.trim());
8254
+ if (!valid.ok) {
8255
+ res.status(400).json({ error: valid.error });
8256
+ return;
8257
+ }
8258
+ jobs[idx].model = updates.model.trim();
8259
+ }
8260
+ else {
8261
+ delete jobs[idx].model;
8262
+ }
8263
+ }
8264
+ if (updates.alwaysDeliver !== undefined) {
8265
+ if (updates.alwaysDeliver === true || updates.alwaysDeliver === 'true') {
8266
+ jobs[idx].always_deliver = true;
8267
+ // Defensive: remove the camelCase form if a prior hand-edit
8268
+ // left one behind. Avoids confusing two-key state.
8269
+ delete jobs[idx].alwaysDeliver;
8270
+ }
8271
+ else {
8272
+ delete jobs[idx].always_deliver;
8273
+ delete jobs[idx].alwaysDeliver;
8274
+ }
8275
+ }
8276
+ if (updates.lean !== undefined) {
8277
+ if (updates.lean === true || updates.lean === 'true') {
8278
+ jobs[idx].lean = true;
8279
+ }
8280
+ else {
8281
+ delete jobs[idx].lean;
8282
+ }
8283
+ }
8173
8284
  writeCronFileAt(cronFile, parsed, jobs);
8174
8285
  res.json({ ok: true, message: `Updated cron job: ${jobs[idx].name}` });
8175
8286
  }
@@ -9652,6 +9763,89 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
9652
9763
  // direction. Soft-delete via deleted_at; FTS trigger keeps deleted
9653
9764
  // content out of search results.
9654
9765
  // Memory Health snapshot — single endpoint feeding the dashboard tab.
9766
+ // ── Reconstitution snapshot (1.18.185 Phase 2) ──────────────────────
9767
+ //
9768
+ // What Clementine would see RIGHT NOW if a chat turn fired for the
9769
+ // given sessionKey. Constructs (doesn't replay) the cacheable system-
9770
+ // prompt append, the volatile turn-context block, the tool surface,
9771
+ // and the permission mode — the same components run-agent.ts +
9772
+ // router.ts assemble at chat time. This is the canonical visibility
9773
+ // surface for verifying 1.18.184's reconstitution actually loads
9774
+ // everything we expect.
9775
+ app.get('/api/clementine/reconstitution', async (req, res) => {
9776
+ try {
9777
+ const sessionKey = typeof req.query.sessionKey === 'string' && req.query.sessionKey
9778
+ ? req.query.sessionKey
9779
+ : 'dashboard:web';
9780
+ const userMessage = typeof req.query.userMessage === 'string'
9781
+ ? req.query.userMessage
9782
+ : '';
9783
+ const gateway = await getGateway();
9784
+ // assistant.getMemoryStore is the public accessor (the field
9785
+ // itself is private). Same pattern router.ts:2542 uses for the
9786
+ // chat-time turn-context build.
9787
+ const memoryStore = gateway
9788
+ .assistant?.getMemoryStore?.() ?? null;
9789
+ const { buildChatSystemAppend } = await import('../agent/run-agent-context.js');
9790
+ const { buildClementineTurnContext } = await import('../agent/clementine-turn-context.js');
9791
+ const { listBackgroundTasks } = await import('../agent/background-tasks.js');
9792
+ // Cacheable system-prompt append (identity + posture).
9793
+ const systemAppend = buildChatSystemAppend({});
9794
+ // Volatile per-turn context for the given user message (or a
9795
+ // generic probe message when none provided — gives a useful view
9796
+ // even before the user has typed anything).
9797
+ const probeMessage = userMessage.trim() || 'show me what you remember about my work';
9798
+ const turnCtx = buildClementineTurnContext({
9799
+ userMessage: probeMessage,
9800
+ sessionKey,
9801
+ channel: sessionKey.split(':')[0] ?? 'chat',
9802
+ memoryStore: memoryStore,
9803
+ listBackgroundTasks,
9804
+ });
9805
+ // Tool surface — the same CORE_TOOLS list runAgent uses, plus a
9806
+ // note that MCP wildcard widens this at runtime via the
9807
+ // Clementine MCP server.
9808
+ const coreTools = [
9809
+ 'Agent', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash',
9810
+ 'WebSearch', 'WebFetch', 'TodoWrite',
9811
+ 'mcp__clementine-tools__memory_search',
9812
+ 'mcp__clementine-tools__transcript_search',
9813
+ 'mcp__clementine-tools__memory_write',
9814
+ 'mcp__clementine-tools__note_take',
9815
+ 'mcp__clementine-tools__note_create',
9816
+ 'mcp__clementine-tools__task_add',
9817
+ ];
9818
+ res.json({
9819
+ ok: true,
9820
+ sessionKey,
9821
+ probeMessage,
9822
+ systemAppend: {
9823
+ text: systemAppend,
9824
+ chars: systemAppend.length,
9825
+ cacheable: true,
9826
+ },
9827
+ turnContext: {
9828
+ block: turnCtx.block,
9829
+ chars: turnCtx.totalChars,
9830
+ sections: turnCtx.sections,
9831
+ cacheable: false,
9832
+ },
9833
+ toolSurface: {
9834
+ coreTools,
9835
+ note: 'MCP wildcard (mcp__clementine-tools__*) widens this at runtime when the MCP server initializes correctly. Skill auto-match (score ≥ 4) can further widen for matched integrations.',
9836
+ },
9837
+ permissionMode: {
9838
+ chat: 'bypassPermissions',
9839
+ autonomous: 'dontAsk',
9840
+ note: 'Chat = trusted local agent (1.18.184). Autonomous (cron / scheduled-skill / heartbeat / team-task) intentionally stays on the stricter dontAsk allowlist.',
9841
+ },
9842
+ capturedAt: new Date().toISOString(),
9843
+ });
9844
+ }
9845
+ catch (err) {
9846
+ res.status(500).json({ ok: false, error: String(err) });
9847
+ }
9848
+ });
9655
9849
  // Read-only aggregate over the existing tables; no caching needed (cheap).
9656
9850
  // Self-correction stats — supersession provenance. Powers the
9657
9851
  // "Self-correction (supersedes)" card on Brain → Health.
@@ -19727,6 +19921,12 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19727
19921
  <button data-icon="zap" onclick="switchTab('intelligence','health')"><span class="icon-slot"></span> Health <span class="tab-badge" id="brain-health-badge" style="display:none;background:#ef4444;color:#fff">0</span></button>
19728
19922
  <button data-icon="users" onclick="switchTab('intelligence','user-model')"><span class="icon-slot"></span> User Model</button>
19729
19923
  <button data-icon="brain" onclick="switchTab('intelligence','learning')"><span class="icon-slot"></span> Learning <span class="tab-badge" id="brain-learning-badge" style="display:none;background:#f59e0b;color:#000">0</span></button>
19924
+ <!-- 1.18.185 Phase 2 — "Reconstitution" tab. Shows what
19925
+ Clementine actually sees on each chat turn (cacheable system
19926
+ prompt, volatile turn-context, tool surface, permission
19927
+ mode). The single most valuable visibility surface for
19928
+ verifying 1.18.184 is working as designed. -->
19929
+ <button data-icon="eye" onclick="switchTab('intelligence','reconstitution')"><span class="icon-slot"></span> Reconstitution</button>
19730
19930
  </div>
19731
19931
  <div id="intelligence-tab-content">
19732
19932
  <div class="tab-pane active" id="tab-intelligence-overview">
@@ -20303,6 +20503,22 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
20303
20503
  <div class="card-body" id="si-history-list"><div class="empty-state">No experiments yet</div></div>
20304
20504
  </div>
20305
20505
  </div>
20506
+ <!-- 1.18.185 Phase 2 — Reconstitution tab. The single most
20507
+ valuable visibility surface for verifying 1.18.184 works
20508
+ as designed: shows what Clementine actually sees on each
20509
+ chat turn. -->
20510
+ <div class="tab-pane" id="tab-intelligence-reconstitution">
20511
+ <div style="margin-bottom:14px;font-size:13px;color:var(--text-secondary);max-width:760px;line-height:1.6">
20512
+ On every chat turn, Clementine is reconstituted from three SDK channels: the cacheable system prompt (her identity, posture, and curated memory), the volatile per-turn context (live SQLite memory hits + recent background work + identity framing), and the tool surface (memory tools + integrations always present). This tab shows you exactly what landed in each channel for the most recent chat turn — the canonical way to debug "why didn't she know X" or "why did she do Y."
20513
+ </div>
20514
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:14px;flex-wrap:wrap">
20515
+ <button class="btn-sm btn-primary" onclick="refreshReconstitution()" id="reconstitution-refresh-btn">Refresh</button>
20516
+ <span id="reconstitution-status" style="font-size:11px;color:var(--text-muted)"></span>
20517
+ </div>
20518
+ <div id="reconstitution-content">
20519
+ <div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>
20520
+ </div>
20521
+ </div>
20306
20522
  </div>
20307
20523
 
20308
20524
  <script>
@@ -22693,6 +22909,42 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
22693
22909
  <div class="form-hint">Trigger after another job succeeds (ignores schedule).</div>
22694
22910
  </div>
22695
22911
  </div>
22912
+ <!-- 1.18.185 — Model override + alwaysDeliver + lean mode. These
22913
+ were parsed from CRON.md and threaded through the runtime
22914
+ but had no dashboard UI to set them. Now exposed here so
22915
+ you don't have to hand-edit CRON.md for per-job control. -->
22916
+ <div class="form-row">
22917
+ <div class="form-group">
22918
+ <label class="form-label">Model <span style="color:var(--text-muted);font-weight:normal">(optional override)</span></label>
22919
+ <select id="cron-model">
22920
+ <option value="">Default — plain Sonnet (200K, on-meter)</option>
22921
+ <option value="sonnet">Sonnet (200K)</option>
22922
+ <option value="claude-sonnet-4-6[1m]">Sonnet [1m] — Extra Usage on Max ⚠</option>
22923
+ <option value="opus">Opus (200K) — Max-covered</option>
22924
+ <option value="claude-opus-4-7[1m]">Opus [1m] — Max-covered long context</option>
22925
+ <option value="haiku">Haiku (cheap)</option>
22926
+ </select>
22927
+ <div class="form-hint">Override the autonomous default. Sonnet [1m] lives on Anthropic's Extra Usage path even with Max — prefer Opus [1m] for long-context work on a Max plan.</div>
22928
+ </div>
22929
+ </div>
22930
+ <div class="form-row">
22931
+ <div class="form-group">
22932
+ <label class="form-label" style="display:flex;align-items:center;gap:8px;cursor:pointer">
22933
+ <input type="checkbox" id="cron-always-deliver" style="margin:0">
22934
+ <span>Retry if response is empty / noise-only</span>
22935
+ </label>
22936
+ <div class="form-hint" style="margin-left:24px">When ON, the scheduler treats whitespace-only or refusal-style responses as failures and retries (up to <code>Max Retries</code> times).</div>
22937
+ </div>
22938
+ </div>
22939
+ <div class="form-row">
22940
+ <div class="form-group">
22941
+ <label class="form-label" style="display:flex;align-items:center;gap:8px;cursor:pointer">
22942
+ <input type="checkbox" id="cron-lean" style="margin:0">
22943
+ <span>Lean envelope (meta-jobs only)</span>
22944
+ </label>
22945
+ <div class="form-hint" style="margin-left:24px">Drops auto-injected context (memory, progress, goal, criteria, skills) and prunes the MCP catalog. Use this for tasks that must stay under Haiku's prompt cap or that should run with a strict envelope. Leave OFF for most jobs.</div>
22946
+ </div>
22947
+ </div>
22696
22948
  </div>
22697
22949
 
22698
22950
  <!-- Training Chat — visible across all config tabs (it's a tool, not a field group) -->
@@ -24168,6 +24420,8 @@ function switchTab(group, tab) {
24168
24420
  if (typeof refreshRoutingAudit === 'function') refreshRoutingAudit();
24169
24421
  }
24170
24422
  if (tab === 'learning' && typeof refreshSelfImprove === 'function') refreshSelfImprove();
24423
+ // 1.18.185 Phase 2 — Reconstitution tab fires its own loader.
24424
+ if (tab === 'reconstitution' && typeof refreshReconstitution === 'function') refreshReconstitution();
24171
24425
  }
24172
24426
  if (group === 'settings') {
24173
24427
  if (tab === 'general' && typeof refreshSettings === 'function') refreshSettings();
@@ -26147,6 +26401,24 @@ function renderScheduledTaskCard(task) {
26147
26401
  if (task.mode === 'unleashed') badges += '<span class="badge badge-purple">long-running</span>';
26148
26402
  if (task.after) badges += '<span class="badge badge-yellow" title="Triggered after ' + esc(task.after) + '">after ' + esc(task.after) + '</span>';
26149
26403
  if (task.maxRetries != null) badges += '<span class="badge badge-gray">' + esc(task.maxRetries) + ' retries</span>';
26404
+ // 1.18.185 — model override badge. Only renders when an override is
26405
+ // set; default (plain Sonnet) stays invisible. Highlights [1m] variant
26406
+ // in orange because that is the Extra Usage path on Anthropic billing
26407
+ // (Max subscription does not comp it). See feedback_sonnet_1m_extra_usage.
26408
+ if (task.model && String(task.model).trim()) {
26409
+ var modelStr = String(task.model);
26410
+ var isExtraUsage = /\[1m\]/i.test(modelStr) && /sonnet/i.test(modelStr);
26411
+ var modelClass = isExtraUsage ? 'badge-orange' : 'badge-blue';
26412
+ var modelTitle = isExtraUsage
26413
+ ? 'Sonnet [1m] = Extra Usage path on Anthropic billing (not covered by Max). Click Edit to change.'
26414
+ : 'Model override active. Click Edit to change.';
26415
+ badges += '<span class="badge ' + modelClass + '" title="' + esc(modelTitle) + '">model: ' + esc(modelStr) + '</span>';
26416
+ }
26417
+ // 1.18.185 — alwaysDeliver badge. Indicates "retry on empty/noise
26418
+ // response" is enabled. Hidden by default.
26419
+ if (task.alwaysDeliver === true) {
26420
+ badges += '<span class="badge badge-gray" title="Retry on empty or noise-only response (up to maxRetries times).">retry-if-empty</span>';
26421
+ }
26150
26422
  badges += operationUsageBadge(task.usage);
26151
26423
  badges += '<span class="badge ' + (enabled ? 'badge-green' : 'badge-gray') + '">' + (enabled ? 'Enabled' : 'Disabled') + '</span>';
26152
26424
  // 1.18.118 — only emit the health badge when it adds new information.
@@ -30668,7 +30940,10 @@ async function loadSkillQualityState(skillName) {
30668
30940
  var d = await r.json();
30669
30941
  if (!r.ok || d.ok === false || !d.score) return;
30670
30942
  var s = d.score;
30671
- var gradeColors = { good: '#10b981', underperforming: '#ef4444', stale: '#f59e0b', 'no-data': '#6b7280' };
30943
+ // 1.18.185 'ready' grade for vault-known skills that haven't run yet.
30944
+ // Blue-ish so it reads as "loaded and waiting" rather than "broken"
30945
+ // (which is how the old 'no-data' badge read on a freshly-created skill).
30946
+ var gradeColors = { good: '#10b981', underperforming: '#ef4444', stale: '#f59e0b', 'no-data': '#6b7280', ready: '#3b82f6' };
30672
30947
  var gradeLabel = (s.grade || 'no-data').replace(/-/g, ' ');
30673
30948
  var color = gradeColors[s.grade] || '#6b7280';
30674
30949
  var pct = function(v) { return v === null || v === undefined ? '—' : (v * 100).toFixed(0) + '%'; };
@@ -32726,6 +33001,13 @@ function openEditCronModal(jobName) {
32726
33001
  document.getElementById('cron-mode').value = job.mode || 'standard';
32727
33002
  document.getElementById('cron-maxhours').value = String(job.max_hours || 6);
32728
33003
  document.getElementById('cron-max-retries').value = job.max_retries != null ? String(job.max_retries) : '';
33004
+ // 1.18.185 — model override + alwaysDeliver + lean mode.
33005
+ var modelEl = document.getElementById('cron-model');
33006
+ if (modelEl) modelEl.value = job.model || '';
33007
+ var alwaysDelEl = document.getElementById('cron-always-deliver');
33008
+ if (alwaysDelEl) alwaysDelEl.checked = (job.alwaysDeliver === true || job.alwaysDeliver === 'true');
33009
+ var leanEl = document.getElementById('cron-lean');
33010
+ if (leanEl) leanEl.checked = (job.lean === true || job.lean === 'true');
32729
33011
  populateAfterJobDropdown(job.after || '', jobName);
32730
33012
  toggleUnleashedOptions();
32731
33013
  document.getElementById('cron-prompt').value = job.prompt || '';
@@ -33242,6 +33524,11 @@ async function saveCronJob() {
33242
33524
  toast('Heads up: add_dirs entries should be absolute paths.', 'info');
33243
33525
  }
33244
33526
 
33527
+ // 1.18.185 — read the new fields.
33528
+ const modelVal = (document.getElementById('cron-model')?.value || '').trim();
33529
+ const alwaysDeliverVal = !!document.getElementById('cron-always-deliver')?.checked;
33530
+ const leanVal = !!document.getElementById('cron-lean')?.checked;
33531
+
33245
33532
  const body = {
33246
33533
  name, schedule, tier, prompt, enabled: true,
33247
33534
  work_dir: work_dir || undefined, mode, max_hours, max_retries, after, context,
@@ -33263,6 +33550,13 @@ async function saveCronJob() {
33263
33550
  successCriteriaText: editingCronJob ? successCriteriaText : (successCriteriaText || undefined),
33264
33551
  successSchema: editingCronJob ? (successSchema || null) : (successSchema || undefined),
33265
33552
  addDirs: editingCronJob ? addDirs : (addDirs.length ? addDirs : undefined),
33553
+ // 1.18.185 — new fields. Edit-mode passes the literal value (so an
33554
+ // empty string clears the YAML key); create-mode passes undefined
33555
+ // so the key is only written when explicitly set, matching the
33556
+ // pattern for the other capability fields.
33557
+ model: editingCronJob ? modelVal : (modelVal || undefined),
33558
+ alwaysDeliver: editingCronJob ? alwaysDeliverVal : (alwaysDeliverVal || undefined),
33559
+ lean: editingCronJob ? leanVal : (leanVal || undefined),
33266
33560
  };
33267
33561
 
33268
33562
  var wasEditing = !!editingCronJob;
@@ -38406,6 +38700,118 @@ async function saveMemoryMd() {
38406
38700
  }
38407
38701
  }
38408
38702
 
38703
+ // ── Reconstitution panel (1.18.185 Phase 2) ──────────────────────────
38704
+ // Shows what Clementine actually sees on each chat turn. Renders the
38705
+ // cacheable system-prompt append, the volatile turn-context block,
38706
+ // the tool surface, and the permission mode side-by-side so the user
38707
+ // can verify the reconstitution is loading what 1.18.184 promised.
38708
+ async function refreshReconstitution() {
38709
+ var el = document.getElementById('reconstitution-content');
38710
+ var statusEl = document.getElementById('reconstitution-status');
38711
+ var btn = document.getElementById('reconstitution-refresh-btn');
38712
+ if (!el) return;
38713
+ if (btn) btn.setAttribute('disabled', 'disabled');
38714
+ if (statusEl) statusEl.textContent = 'Loading…';
38715
+ try {
38716
+ var r = await apiFetch('/api/clementine/reconstitution?sessionKey=dashboard:web');
38717
+ var d = await r.json();
38718
+ if (!d || d.ok === false) {
38719
+ el.innerHTML = '<div class="empty-state">' + esc(d?.error || 'Failed to load reconstitution snapshot') + '</div>';
38720
+ return;
38721
+ }
38722
+
38723
+ var sa = d.systemAppend || {};
38724
+ var tc = d.turnContext || {};
38725
+ var tcSections = tc.sections || {};
38726
+ var totalChars = (sa.chars || 0) + (tc.chars || 0);
38727
+
38728
+ var html = '';
38729
+
38730
+ // ── Hero: total reconstitution size + cache health framing ─────
38731
+ html += '<div style="margin-bottom:18px;padding:14px 18px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px">';
38732
+ html += '<div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;font-weight:600;margin-bottom:6px">Total reconstitution this turn</div>';
38733
+ html += '<div style="display:flex;gap:24px;flex-wrap:wrap;align-items:flex-end">';
38734
+ html += '<div><div style="font-size:24px;font-weight:700">' + formatBytes(totalChars) + '</div><div style="font-size:11px;color:var(--text-muted)">total characters loaded</div></div>';
38735
+ html += '<div><div style="font-size:18px;font-weight:600;color:var(--green)">' + formatBytes(sa.chars || 0) + '</div><div style="font-size:11px;color:var(--text-muted)">cacheable (system prompt)</div></div>';
38736
+ html += '<div><div style="font-size:18px;font-weight:600;color:var(--blue)">' + formatBytes(tc.chars || 0) + '</div><div style="font-size:11px;color:var(--text-muted)">volatile (turn context)</div></div>';
38737
+ html += '</div>';
38738
+ html += '<div style="margin-top:10px;font-size:11px;color:var(--text-muted);line-height:1.5">Anthropic prompt-cache holds the cacheable prefix across turns; only the volatile delta pays per-turn input cost. Healthy ratio: cacheable larger than volatile.</div>';
38739
+ html += '</div>';
38740
+
38741
+ // ── Panel: cacheable system-prompt append ──────────────────────
38742
+ html += '<div class="card" style="margin-bottom:14px">';
38743
+ html += '<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">';
38744
+ html += '<span>System Prompt Append <span style="color:var(--text-muted);font-weight:normal;font-size:11px">· cacheable · ' + formatBytes(sa.chars || 0) + '</span></span>';
38745
+ html += '<span style="font-size:11px;color:var(--green)">stable across turns</span>';
38746
+ html += '</div>';
38747
+ html += '<div class="card-body" style="padding:0">';
38748
+ html += '<pre style="margin:0;padding:14px;background:var(--bg-primary);font-family:monospace;font-size:11px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:360px;overflow:auto;color:var(--text-secondary)">' + esc(sa.text || '') + '</pre>';
38749
+ html += '</div></div>';
38750
+
38751
+ // ── Panel: volatile turn-context block ─────────────────────────
38752
+ html += '<div class="card" style="margin-bottom:14px">';
38753
+ html += '<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">';
38754
+ html += '<span>Turn Context Block <span style="color:var(--text-muted);font-weight:normal;font-size:11px">· volatile (per-turn) · ' + formatBytes(tc.chars || 0) + '</span></span>';
38755
+ html += '<span style="font-size:11px;color:var(--text-muted)">probe: <code>' + esc((d.probeMessage || '').slice(0, 60)) + (d.probeMessage && d.probeMessage.length > 60 ? '…' : '') + '</code></span>';
38756
+ html += '</div>';
38757
+ html += '<div class="card-body" style="padding:0">';
38758
+
38759
+ // Section badges
38760
+ html += '<div style="padding:10px 14px;border-bottom:1px solid var(--border);display:flex;gap:6px;flex-wrap:wrap;font-size:11px">';
38761
+ var hits = tcSections.retrievedMemory || 0;
38762
+ var bgN = tcSections.recentBgTasks || 0;
38763
+ html += '<span class="badge ' + (hits > 0 ? 'badge-green' : 'badge-gray') + '" title="Top semantic + FTS hits from SQLite for this turn">' + hits + ' memory hit' + (hits === 1 ? '' : 's') + '</span>';
38764
+ html += '<span class="badge ' + (bgN > 0 ? 'badge-blue' : 'badge-gray') + '" title="Terminal-state bg tasks from last 24h">' + bgN + ' recent bg task' + (bgN === 1 ? '' : 's') + '</span>';
38765
+ if (tcSections.identityFrame) html += '<span class="badge badge-purple">identity frame</span>';
38766
+ if (tcSections.liveState) html += '<span class="badge badge-gray">live state</span>';
38767
+ html += '</div>';
38768
+
38769
+ if (!tc.block) {
38770
+ html += '<div class="empty-state" style="padding:14px">No turn-context generated. This is normal when the probe message has no relevant memory hits AND there are no recent bg tasks. Live state (date/time) will appear once you ask Clementine something real.</div>';
38771
+ } else {
38772
+ html += '<pre style="margin:0;padding:14px;background:var(--bg-primary);font-family:monospace;font-size:11px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:360px;overflow:auto;color:var(--text-secondary)">' + esc(tc.block) + '</pre>';
38773
+ }
38774
+ html += '</div></div>';
38775
+
38776
+ // ── Panel: tool surface ────────────────────────────────────────
38777
+ var ts = d.toolSurface || {};
38778
+ var tools = Array.isArray(ts.coreTools) ? ts.coreTools : [];
38779
+ html += '<div class="card" style="margin-bottom:14px">';
38780
+ html += '<div class="card-header">Tool Surface <span style="color:var(--text-muted);font-weight:normal;font-size:11px">· ' + tools.length + ' core + MCP wildcard</span></div>';
38781
+ html += '<div class="card-body">';
38782
+ html += '<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px">';
38783
+ for (var ti = 0; ti < tools.length; ti++) {
38784
+ var t = tools[ti];
38785
+ var isMemory = String(t).indexOf('mcp__clementine-tools__') === 0;
38786
+ var cls = isMemory ? 'badge-blue' : 'badge-gray';
38787
+ html += '<span class="badge ' + cls + '">' + esc(t) + '</span>';
38788
+ }
38789
+ html += '</div>';
38790
+ if (ts.note) html += '<div style="font-size:11px;color:var(--text-muted);line-height:1.5">' + esc(ts.note) + '</div>';
38791
+ html += '</div></div>';
38792
+
38793
+ // ── Panel: permission mode ─────────────────────────────────────
38794
+ var pm = d.permissionMode || {};
38795
+ html += '<div class="card">';
38796
+ html += '<div class="card-header">Permission Mode</div>';
38797
+ html += '<div class="card-body">';
38798
+ html += '<div style="display:flex;gap:18px;flex-wrap:wrap;margin-bottom:10px">';
38799
+ html += '<div><div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:4px">Chat</div><span class="badge badge-green">' + esc(pm.chat || 'unknown') + '</span></div>';
38800
+ html += '<div><div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:4px">Autonomous</div><span class="badge badge-gray">' + esc(pm.autonomous || 'unknown') + '</span></div>';
38801
+ html += '</div>';
38802
+ if (pm.note) html += '<div style="font-size:11px;color:var(--text-muted);line-height:1.5">' + esc(pm.note) + '</div>';
38803
+ html += '</div></div>';
38804
+
38805
+ el.innerHTML = html;
38806
+ if (statusEl) statusEl.textContent = 'Captured at ' + new Date(d.capturedAt).toLocaleTimeString();
38807
+ } catch (err) {
38808
+ el.innerHTML = '<div class="empty-state">Failed to load: ' + esc(String(err)) + '</div>';
38809
+ if (statusEl) statusEl.textContent = '';
38810
+ } finally {
38811
+ if (btn) btn.removeAttribute('disabled');
38812
+ }
38813
+ }
38814
+
38409
38815
  async function refreshMemoryHealth() {
38410
38816
  var el = document.getElementById('memory-health-content');
38411
38817
  if (!el) return;
@@ -150,7 +150,12 @@ function parseJobYaml(job) {
150
150
  ? successSchemaRaw
151
151
  : undefined;
152
152
  const addDirs = normalizeStringArray(job.add_dirs ?? job.addDirs);
153
- const alwaysDeliver = job.always_deliver === true ? true : undefined;
153
+ // 1.18.185 accept both casings. The dashboard writes the canonical
154
+ // snake_case (matches work_dir / max_hours / allowed_tools etc.) but
155
+ // hand-edited CRON.md files in the wild predate the dashboard write
156
+ // path; some may have used camelCase. Defensive parsing here protects
157
+ // against silent breakage from either source.
158
+ const alwaysDeliver = (job.always_deliver === true || job.alwaysDeliver === true) ? true : undefined;
154
159
  const context = job.context != null ? String(job.context) : undefined;
155
160
  const preCheck = job.pre_check != null ? String(job.pre_check) : undefined;
156
161
  const attachments = normalizeStringArray(job.attachments);
@@ -63,13 +63,16 @@ export interface SkillQualityScore {
63
63
  /** Most recent ISO timestamp this skill was applied to a run. */
64
64
  lastUsedAt: string | null;
65
65
  /**
66
- * Coarse 4-bucket label for owner attention:
66
+ * Coarse 5-bucket label for owner attention:
67
67
  * - 'good' — enough runs, success rate above threshold
68
68
  * - 'underperforming' — enough runs, success rate below threshold
69
69
  * - 'stale' — no runs in the last STALE_DAYS regardless of past stats
70
70
  * - 'no-data' — fewer than MIN_RUNS_FOR_GRADE runs in the window
71
+ * - 'ready' (1.18.185) — skill exists in the vault but has never run,
72
+ * so we have no observations to grade against. Distinguished from
73
+ * 'no-data' so freshly-created skills don't look broken on the UI.
71
74
  */
72
- grade: 'good' | 'underperforming' | 'stale' | 'no-data';
75
+ grade: 'good' | 'underperforming' | 'stale' | 'no-data' | 'ready';
73
76
  /** One-sentence reason for the grade — surfaces under the badge. */
74
77
  gradeReason: string;
75
78
  }
@@ -85,12 +88,20 @@ export declare function computeSkillQuality(skillName: string, options?: {
85
88
  /**
86
89
  * Compute scores for every skill that appeared in *any* run within the
87
90
  * window. Returns one score per skill name, sorted by totalRuns desc
88
- * (most-used first). Skills that exist in the vault but never ran will
89
- * not appear — callers that need "every skill" should merge with the
90
- * skill-store listing themselves.
91
+ * (most-used first).
92
+ *
93
+ * 1.18.185 pass `vaultSkillNames` to merge in skills that exist on
94
+ * disk but have never run; those get a synthetic 'ready' grade so
95
+ * freshly-created skills don't get rendered with the (misleading)
96
+ * 'no-data' badge that previously meant "we don't know yet" but read
97
+ * to users as "this thing is broken."
91
98
  */
92
99
  export declare function computeAllSkillQuality(options?: {
93
100
  windowDays?: number;
94
101
  baseDir?: string;
102
+ /** Optional: list of skill names known to exist in the vault.
103
+ * Names in this list that have ZERO runs get a synthetic 'ready'
104
+ * grade. Without this, never-run skills don't appear at all. */
105
+ vaultSkillNames?: string[];
95
106
  }): SkillQualityScore[];
96
107
  //# sourceMappingURL=skill-quality.d.ts.map
@@ -202,9 +202,13 @@ export function computeSkillQuality(skillName, options = {}) {
202
202
  /**
203
203
  * Compute scores for every skill that appeared in *any* run within the
204
204
  * window. Returns one score per skill name, sorted by totalRuns desc
205
- * (most-used first). Skills that exist in the vault but never ran will
206
- * not appear — callers that need "every skill" should merge with the
207
- * skill-store listing themselves.
205
+ * (most-used first).
206
+ *
207
+ * 1.18.185 pass `vaultSkillNames` to merge in skills that exist on
208
+ * disk but have never run; those get a synthetic 'ready' grade so
209
+ * freshly-created skills don't get rendered with the (misleading)
210
+ * 'no-data' badge that previously meant "we don't know yet" but read
211
+ * to users as "this thing is broken."
208
212
  */
209
213
  export function computeAllSkillQuality(options = {}) {
210
214
  const windowDays = options.windowDays ?? DEFAULT_WINDOW_DAYS;
@@ -222,10 +226,40 @@ export function computeAllSkillQuality(options = {}) {
222
226
  for (const name of seen) {
223
227
  scores.push(computeSkillQuality(name, options));
224
228
  }
229
+ // 1.18.185 — merge in vault-known skills that have never run with a
230
+ // synthetic 'ready' grade. We don't compute the full metrics for
231
+ // these (they're all null/0); the grade carries the signal that the
232
+ // skill is loaded and waiting.
233
+ if (options.vaultSkillNames && options.vaultSkillNames.length > 0) {
234
+ for (const name of options.vaultSkillNames) {
235
+ if (seen.has(name))
236
+ continue;
237
+ scores.push(buildReadyScore(name, windowDays));
238
+ }
239
+ }
225
240
  scores.sort((a, b) => b.totalRuns - a.totalRuns || a.name.localeCompare(b.name));
226
241
  if (scores.length > 0) {
227
242
  logger.debug({ count: scores.length, top: scores[0]?.name, topRuns: scores[0]?.totalRuns }, 'Skill quality scored');
228
243
  }
229
244
  return scores;
230
245
  }
246
+ /** Synthetic score for a vault-known skill that has never executed. */
247
+ function buildReadyScore(name, windowDays) {
248
+ return {
249
+ name,
250
+ windowDays,
251
+ totalRuns: 0,
252
+ pinnedRuns: 0,
253
+ autoRuns: 0,
254
+ successRuns: 0,
255
+ failureRuns: 0,
256
+ successRate: null,
257
+ triggerAccuracy: null,
258
+ avgDurationMs: null,
259
+ avgCostUsd: null,
260
+ lastUsedAt: null,
261
+ grade: 'ready',
262
+ gradeReason: 'Skill is loaded and ready to use. No runs yet — grade will populate after the first execution.',
263
+ };
264
+ }
231
265
  //# sourceMappingURL=skill-quality.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.184",
3
+ "version": "1.18.185",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",