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.
package/dist/cli/dashboard.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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).
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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).
|
|
206
|
-
*
|
|
207
|
-
*
|
|
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
|