clementine-agent 1.18.154 → 1.18.156
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
|
@@ -5136,21 +5136,33 @@ export async function cmdDashboard(opts) {
|
|
|
5136
5136
|
res.status(500).json({ ok: false, error: String(err) });
|
|
5137
5137
|
}
|
|
5138
5138
|
});
|
|
5139
|
-
app.post('/api/cron/migrate-all-to-skills', async (
|
|
5139
|
+
app.post('/api/cron/migrate-all-to-skills', async (req, res) => {
|
|
5140
5140
|
try {
|
|
5141
|
-
const { findMatchingSkill } = await import('../agent/cron-migrator.js');
|
|
5142
|
-
const { listSkills } = await import('../agent/skill-store.js');
|
|
5141
|
+
const { findMatchingSkill, generateDescription } = await import('../agent/cron-migrator.js');
|
|
5142
|
+
const { listSkills, writeSkill } = await import('../agent/skill-store.js');
|
|
5143
5143
|
const { setSchedule } = await import('../agent/schedule-registry.js');
|
|
5144
5144
|
const { parseCronJobs, parseAgentCronJobs } = await import('../gateway/cron-scheduler.js');
|
|
5145
|
+
// 1.18.156 — `createMissing` defaults true so the bulk path actually
|
|
5146
|
+
// converts every legacy cron (auto-generates a folder-form skill from
|
|
5147
|
+
// the cron prompt when no match exists). The user can still pass
|
|
5148
|
+
// `{createMissing: false}` if they only want safe matches. .bak
|
|
5149
|
+
// backups still happen either way.
|
|
5150
|
+
const body = (req.body ?? {});
|
|
5151
|
+
const createMissing = body.createMissing !== false;
|
|
5145
5152
|
const skills = listSkills();
|
|
5146
5153
|
const allJobs = [...parseCronJobs(), ...parseAgentCronJobs(path.join(VAULT_DIR, '00-System', 'agents'))];
|
|
5147
5154
|
const legacy = allJobs.filter(j => j.source !== 'scheduled-skill');
|
|
5148
5155
|
const migrated = [];
|
|
5149
5156
|
const skipped = [];
|
|
5157
|
+
// Track names we've just created so subsequent jobs in the loop don't
|
|
5158
|
+
// bail with "skill not in catalog" before listSkills() refreshes.
|
|
5159
|
+
const createdThisPass = new Set();
|
|
5160
|
+
const inCatalog = (n) => createdThisPass.has(n) || skills.some(s => s.frontmatter.name === n);
|
|
5150
5161
|
// Group by source CRON.md so we open + parse + write each file once.
|
|
5151
5162
|
const byFile = new Map();
|
|
5152
5163
|
for (const job of legacy) {
|
|
5153
5164
|
let skillName = null;
|
|
5165
|
+
let created = false;
|
|
5154
5166
|
if (job.skills && job.skills.length > 0) {
|
|
5155
5167
|
skillName = job.skills.find(s => skills.some(sk => sk.frontmatter.name === s)) ?? null;
|
|
5156
5168
|
}
|
|
@@ -5160,10 +5172,49 @@ export async function cmdDashboard(opts) {
|
|
|
5160
5172
|
skillName = m.frontmatter.name;
|
|
5161
5173
|
}
|
|
5162
5174
|
if (!skillName) {
|
|
5163
|
-
|
|
5164
|
-
|
|
5175
|
+
if (!createMissing) {
|
|
5176
|
+
skipped.push({ name: job.name, reason: 'no skill match (set createMissing=true to auto-create)' });
|
|
5177
|
+
continue;
|
|
5178
|
+
}
|
|
5179
|
+
// Auto-create a skill from the cron's prompt. Mirrors the per-row
|
|
5180
|
+
// /migrate-with-skill-creation endpoint logic.
|
|
5181
|
+
const candidate = job.name
|
|
5182
|
+
.toLowerCase().replace(/[_\s]+/g, '-').replace(/[^a-z0-9-]/g, '')
|
|
5183
|
+
.replace(/-+/g, '-').replace(/^-+|-+$/g, '')
|
|
5184
|
+
.replace(/-(cron|task|job)$/, '').slice(0, 64);
|
|
5185
|
+
if (!candidate || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(candidate) || inCatalog(candidate)) {
|
|
5186
|
+
skipped.push({ name: job.name, reason: `cannot derive a unique kebab-case name (candidate: "${candidate}")` });
|
|
5187
|
+
continue;
|
|
5188
|
+
}
|
|
5189
|
+
const cronPrompt = String(job.prompt || '').trim();
|
|
5190
|
+
const desc = generateDescription(cronPrompt, null);
|
|
5191
|
+
const triggerPhrase = job.name.replace(/-/g, ' ');
|
|
5192
|
+
const description = `${desc} Use when this scheduled task fires (cron: ${job.schedule}) or when the user mentions "${triggerPhrase}".`.slice(0, 1024);
|
|
5193
|
+
try {
|
|
5194
|
+
writeSkill({
|
|
5195
|
+
name: candidate,
|
|
5196
|
+
description,
|
|
5197
|
+
body: [
|
|
5198
|
+
`# ${candidate.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}`,
|
|
5199
|
+
'', '## Instructions', '',
|
|
5200
|
+
cronPrompt || '(prompt not captured — fill in)',
|
|
5201
|
+
'', '## When to use', '',
|
|
5202
|
+
`Auto-fired on schedule \`${job.schedule}\`. Also matches user prompts about "${triggerPhrase}".`,
|
|
5203
|
+
'',
|
|
5204
|
+
].join('\n'),
|
|
5205
|
+
source: 'imported',
|
|
5206
|
+
...(job.agentSlug ? { agentSlug: job.agentSlug } : {}),
|
|
5207
|
+
});
|
|
5208
|
+
createdThisPass.add(candidate);
|
|
5209
|
+
skillName = candidate;
|
|
5210
|
+
created = true;
|
|
5211
|
+
}
|
|
5212
|
+
catch (writeErr) {
|
|
5213
|
+
skipped.push({ name: job.name, reason: `skill auto-create failed: ${String(writeErr)}` });
|
|
5214
|
+
continue;
|
|
5215
|
+
}
|
|
5165
5216
|
}
|
|
5166
|
-
if (!
|
|
5217
|
+
if (!inCatalog(skillName)) {
|
|
5167
5218
|
skipped.push({ name: job.name, reason: `skill "${skillName}" not in catalog` });
|
|
5168
5219
|
continue;
|
|
5169
5220
|
}
|
|
@@ -5173,7 +5224,7 @@ export async function cmdDashboard(opts) {
|
|
|
5173
5224
|
enabled: job.enabled !== false,
|
|
5174
5225
|
agentSlug: job.agentSlug ?? null,
|
|
5175
5226
|
});
|
|
5176
|
-
migrated.push({ name: job.name, skillName });
|
|
5227
|
+
migrated.push({ name: job.name, skillName, ...(created ? { created: true } : {}) });
|
|
5177
5228
|
const { cronFile, bareJobName } = resolveJobCronFile(job.name);
|
|
5178
5229
|
if (!byFile.has(cronFile))
|
|
5179
5230
|
byFile.set(cronFile, { jobsToRemove: new Set(), cronFile });
|
|
@@ -5217,6 +5268,136 @@ export async function cmdDashboard(opts) {
|
|
|
5217
5268
|
res.status(500).json({ ok: false, error: String(err) });
|
|
5218
5269
|
}
|
|
5219
5270
|
});
|
|
5271
|
+
// 1.18.156 — Per-row "create skill from this cron prompt + schedule it"
|
|
5272
|
+
// path. The user's legacy crons usually have no matching skill (so the
|
|
5273
|
+
// existing /api/cron/:job/migrate-to-skill aborts with "Create a skill
|
|
5274
|
+
// first"). This endpoint closes that gap by writing a folder-form
|
|
5275
|
+
// SKILL.md from the cron's prompt, then performing the schedule wire-up
|
|
5276
|
+
// in one shot. Anthropic-canonical: name = kebab-case, description has
|
|
5277
|
+
// WHAT + WHEN + first-sentence trigger phrase, body is the cron prompt
|
|
5278
|
+
// verbatim. The user can immediately refine via the Builder afterward.
|
|
5279
|
+
app.post('/api/cron/:job/migrate-with-skill-creation', async (req, res) => {
|
|
5280
|
+
try {
|
|
5281
|
+
const jobName = req.params.job;
|
|
5282
|
+
if (!jobName) {
|
|
5283
|
+
res.status(400).json({ ok: false, error: 'job name required' });
|
|
5284
|
+
return;
|
|
5285
|
+
}
|
|
5286
|
+
const proposedName = String((req.body && req.body.skillName) || '').trim();
|
|
5287
|
+
const { generateDescription } = await import('../agent/cron-migrator.js');
|
|
5288
|
+
const { writeSkill, listSkills } = await import('../agent/skill-store.js');
|
|
5289
|
+
const { setSchedule } = await import('../agent/schedule-registry.js');
|
|
5290
|
+
const { parseCronJobs, parseAgentCronJobs } = await import('../gateway/cron-scheduler.js');
|
|
5291
|
+
const { cronFile, bareJobName } = resolveJobCronFile(jobName);
|
|
5292
|
+
const allJobs = [...parseCronJobs(), ...parseAgentCronJobs(path.join(VAULT_DIR, '00-System', 'agents'))];
|
|
5293
|
+
const target = allJobs.find(j => String(j.name).toLowerCase() === jobName.toLowerCase());
|
|
5294
|
+
if (!target)
|
|
5295
|
+
return res.status(404).json({ ok: false, error: `job "${jobName}" not found` });
|
|
5296
|
+
if (target.source === 'scheduled-skill') {
|
|
5297
|
+
return res.status(400).json({ ok: false, error: 'already a scheduled skill' });
|
|
5298
|
+
}
|
|
5299
|
+
// Slugify proposed name (or fall back to the job name) to Anthropic's
|
|
5300
|
+
// kebab-case convention. Strip "cron"/"task" suffixes that don't add
|
|
5301
|
+
// meaning; cap at 64 chars per the SKILL.md spec.
|
|
5302
|
+
const rawName = proposedName || jobName;
|
|
5303
|
+
const skillName = rawName
|
|
5304
|
+
.toLowerCase()
|
|
5305
|
+
.replace(/[_\s]+/g, '-')
|
|
5306
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
5307
|
+
.replace(/-+/g, '-')
|
|
5308
|
+
.replace(/^-+|-+$/g, '')
|
|
5309
|
+
.replace(/-(cron|task|job)$/, '')
|
|
5310
|
+
.slice(0, 64);
|
|
5311
|
+
if (!skillName || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(skillName)) {
|
|
5312
|
+
return res.status(400).json({ ok: false, error: `cannot derive a valid kebab-case skill name from "${rawName}"` });
|
|
5313
|
+
}
|
|
5314
|
+
// Collision check.
|
|
5315
|
+
const existing = listSkills();
|
|
5316
|
+
if (existing.some(s => s.frontmatter.name === skillName)) {
|
|
5317
|
+
return res.status(409).json({
|
|
5318
|
+
ok: false,
|
|
5319
|
+
error: `Skill "${skillName}" already exists. Pass a different skillName or use /migrate-to-skill to bind to it.`,
|
|
5320
|
+
});
|
|
5321
|
+
}
|
|
5322
|
+
// Build an Anthropic-canonical SKILL.md. Description follows the
|
|
5323
|
+
// WHAT + WHEN pattern from the PDF (page 11): start with what the
|
|
5324
|
+
// cron does, add a trigger phrase derived from the cron name.
|
|
5325
|
+
const cronPrompt = String(target.prompt || '').trim();
|
|
5326
|
+
const cleanedDesc = generateDescription(cronPrompt, null);
|
|
5327
|
+
const triggerPhrase = jobName.replace(/-/g, ' ');
|
|
5328
|
+
const description = `${cleanedDesc} Use when this scheduled task fires (cron: ${target.schedule}) or when the user mentions "${triggerPhrase}".`.slice(0, 1024);
|
|
5329
|
+
const body = [
|
|
5330
|
+
`# ${skillName.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}`,
|
|
5331
|
+
'',
|
|
5332
|
+
'## Instructions',
|
|
5333
|
+
'',
|
|
5334
|
+
cronPrompt || '(prompt not captured — fill in)',
|
|
5335
|
+
'',
|
|
5336
|
+
'## When to use',
|
|
5337
|
+
'',
|
|
5338
|
+
`Auto-fired on schedule \`${target.schedule}\`. Also matches user prompts about "${triggerPhrase}".`,
|
|
5339
|
+
'',
|
|
5340
|
+
].join('\n');
|
|
5341
|
+
let writeResult;
|
|
5342
|
+
try {
|
|
5343
|
+
writeResult = writeSkill({
|
|
5344
|
+
name: skillName,
|
|
5345
|
+
description,
|
|
5346
|
+
body,
|
|
5347
|
+
source: 'imported',
|
|
5348
|
+
...(target.agentSlug ? { agentSlug: target.agentSlug } : {}),
|
|
5349
|
+
});
|
|
5350
|
+
}
|
|
5351
|
+
catch (writeErr) {
|
|
5352
|
+
return res.status(500).json({ ok: false, error: `skill write failed: ${String(writeErr)}` });
|
|
5353
|
+
}
|
|
5354
|
+
// Now wire the schedule pointing at the new skill.
|
|
5355
|
+
const entry = setSchedule(skillName, {
|
|
5356
|
+
schedule: target.schedule,
|
|
5357
|
+
enabled: target.enabled !== false,
|
|
5358
|
+
agentSlug: target.agentSlug ?? null,
|
|
5359
|
+
});
|
|
5360
|
+
// Remove the legacy cron entry. Same .bak + write-back pattern as the
|
|
5361
|
+
// canonical /migrate-to-skill endpoint.
|
|
5362
|
+
const bakPath = cronFile + '.bak';
|
|
5363
|
+
try {
|
|
5364
|
+
writeFileSync(bakPath, readFileSync(cronFile, 'utf-8'));
|
|
5365
|
+
}
|
|
5366
|
+
catch { /* best-effort */ }
|
|
5367
|
+
const { parsed, jobs } = readCronFileAt(cronFile);
|
|
5368
|
+
const idx = jobs.findIndex(j => String(j.name).toLowerCase() === bareJobName.toLowerCase());
|
|
5369
|
+
if (idx >= 0) {
|
|
5370
|
+
jobs.splice(idx, 1);
|
|
5371
|
+
writeCronFileAt(cronFile, parsed, jobs);
|
|
5372
|
+
}
|
|
5373
|
+
try {
|
|
5374
|
+
const gw = await getGateway();
|
|
5375
|
+
const sched = gw.cronScheduler;
|
|
5376
|
+
if (sched && typeof sched.reloadJobs === 'function')
|
|
5377
|
+
sched.reloadJobs();
|
|
5378
|
+
}
|
|
5379
|
+
catch { /* best-effort */ }
|
|
5380
|
+
try {
|
|
5381
|
+
broadcastEvent({ type: 'skill_created', data: { skillName, fromCron: jobName } });
|
|
5382
|
+
}
|
|
5383
|
+
catch { /* non-fatal */ }
|
|
5384
|
+
try {
|
|
5385
|
+
broadcastEvent({ type: 'cron_deleted', data: { job: jobName, source: 'cron-md', migrated: true } });
|
|
5386
|
+
}
|
|
5387
|
+
catch { /* non-fatal */ }
|
|
5388
|
+
res.json({
|
|
5389
|
+
ok: true,
|
|
5390
|
+
skillName,
|
|
5391
|
+
scheduleEntry: entry,
|
|
5392
|
+
skillPath: writeResult.filePath,
|
|
5393
|
+
bakPath,
|
|
5394
|
+
message: `Created skill "${skillName}" and scheduled it on ${target.schedule}. Cron entry removed (backup: ${bakPath}).`,
|
|
5395
|
+
});
|
|
5396
|
+
}
|
|
5397
|
+
catch (err) {
|
|
5398
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
5399
|
+
}
|
|
5400
|
+
});
|
|
5220
5401
|
app.post('/api/cron/migrate-all', async (_req, res) => {
|
|
5221
5402
|
try {
|
|
5222
5403
|
const { migrateAllEligibleJobs } = await import('../agent/cron-migrator.js');
|
|
@@ -25418,8 +25599,18 @@ function renderScheduledTaskCard(task) {
|
|
|
25418
25599
|
} else {
|
|
25419
25600
|
badges += '<span class="badge badge-gray" title="Legacy CRON.md job. Carries its own prompt/tools/MCP. Convert to a scheduled skill when you can.">LEGACY CRON</span>';
|
|
25420
25601
|
}
|
|
25421
|
-
|
|
25422
|
-
|
|
25602
|
+
// 1.18.155 — replace the "🔒 predictable" lock icon (which appeared on
|
|
25603
|
+
// ~every row and added pure noise) with a single "Strict" pill that ONLY
|
|
25604
|
+
// appears when behavior diverges from the default. predictable=true is
|
|
25605
|
+
// the default for new tasks since 1.18.68, so suppress that badge — only
|
|
25606
|
+
// show one when the user has explicitly opted into dynamic mode (the
|
|
25607
|
+
// less safe path, worth flagging) or when lean mode is in effect for a
|
|
25608
|
+
// meta-job (worth showing because it explains why the prompt is small).
|
|
25609
|
+
if (task.lean === true) {
|
|
25610
|
+
badges += '<span class="badge badge-purple" title="Lean envelope — drops every auto-injected context block (memory, progress, goal, criteria, skills) and prunes the MCP catalog. Used for meta-jobs that must stay under Haiku\'s prompt cap.">Lean</span>';
|
|
25611
|
+
} else if (task.predictable === false) {
|
|
25612
|
+
badges += '<span class="badge badge-yellow" title="Dynamic mode — fire-time injects MEMORY.md, recent team activity, and auto-matched skills. Can drift from chat-time intent.">Reads memory</span>';
|
|
25613
|
+
}
|
|
25423
25614
|
if (task.mode === 'unleashed') badges += '<span class="badge badge-purple">long-running</span>';
|
|
25424
25615
|
if (task.after) badges += '<span class="badge badge-yellow" title="Triggered after ' + esc(task.after) + '">after ' + esc(task.after) + '</span>';
|
|
25425
25616
|
if (task.maxRetries != null) badges += '<span class="badge badge-gray">' + esc(task.maxRetries) + ' retries</span>';
|
|
@@ -25488,42 +25679,82 @@ function renderScheduledTaskCard(task) {
|
|
|
25488
25679
|
}
|
|
25489
25680
|
|
|
25490
25681
|
// 1.18.132 — Phase 3 migrator: convert one legacy cron → scheduled skill.
|
|
25491
|
-
//
|
|
25492
|
-
//
|
|
25682
|
+
// 1.18.156 — End-to-end migration: try /migrate-to-skill first; if the
|
|
25683
|
+
// backend bails with "no skill match" (the common case for the user's
|
|
25684
|
+
// 15 legacy crons), surface a confirm to CREATE the skill from the cron's
|
|
25685
|
+
// prompt and schedule it in one shot via /migrate-with-skill-creation.
|
|
25686
|
+
// Replaces the silent-failure UX where the button did nothing visible.
|
|
25493
25687
|
async function migrateCronToSkillFromCard(jobName) {
|
|
25494
25688
|
if (!confirm('Convert "' + jobName + '" to a scheduled skill?\\n\\n' +
|
|
25495
|
-
'This writes a thin entry in ~/.clementine/schedules.json
|
|
25496
|
-
'and removes the legacy fat-cron entry from CRON.md. '
|
|
25497
|
-
'A .bak backup is left so you can restore if anything looks wrong.')) return;
|
|
25689
|
+
'This writes a thin entry in ~/.clementine/schedules.json pointing to a skill, ' +
|
|
25690
|
+
'and removes the legacy fat-cron entry from CRON.md. A .bak backup is left.')) return;
|
|
25498
25691
|
try {
|
|
25499
25692
|
var r = await apiFetch('/api/cron/' + encodeURIComponent(jobName) + '/migrate-to-skill', { method: 'POST' });
|
|
25500
25693
|
var d = await r.json();
|
|
25501
|
-
if (
|
|
25502
|
-
|
|
25503
|
-
|
|
25694
|
+
if (r.ok) {
|
|
25695
|
+
toast('Migrated "' + jobName + '" → scheduled skill "' + d.skillName + '"', 'success');
|
|
25696
|
+
if (typeof refreshCron === 'function') refreshCron();
|
|
25697
|
+
return;
|
|
25698
|
+
}
|
|
25699
|
+
// Common path: no matching skill exists. Offer to auto-create one.
|
|
25700
|
+
var msg = d.error || '';
|
|
25701
|
+
if (msg.indexOf('No skill match') >= 0 || msg.indexOf('not in catalog') >= 0) {
|
|
25702
|
+
var proceed = confirm(
|
|
25703
|
+
'No matching skill found for "' + jobName + '".\\n\\n' +
|
|
25704
|
+
'Want me to create a folder-form skill from this cron\\x27s prompt and schedule it?\\n\\n' +
|
|
25705
|
+
'• The skill is named after the cron (kebab-case)\\n' +
|
|
25706
|
+
'• Description follows Anthropic\\x27s WHAT + WHEN format\\n' +
|
|
25707
|
+
'• The cron prompt becomes the skill body — refine it later in the Builder\\n' +
|
|
25708
|
+
'• The legacy cron is removed (with .bak backup)'
|
|
25709
|
+
);
|
|
25710
|
+
if (!proceed) { toast('Migration cancelled — no skill created', 'info'); return; }
|
|
25711
|
+
var r2 = await apiFetch('/api/cron/' + encodeURIComponent(jobName) + '/migrate-with-skill-creation', { method: 'POST' });
|
|
25712
|
+
var d2 = await r2.json();
|
|
25713
|
+
if (!r2.ok) { toast(d2.error || 'Skill creation + migration failed', 'error'); return; }
|
|
25714
|
+
toast('Created skill "' + d2.skillName + '" + scheduled it. Refine in the Builder when ready.', 'success');
|
|
25715
|
+
if (typeof refreshCron === 'function') refreshCron();
|
|
25716
|
+
if (typeof refreshSkills === 'function') refreshSkills();
|
|
25717
|
+
return;
|
|
25718
|
+
}
|
|
25719
|
+
toast(msg || 'Migration failed', 'error');
|
|
25504
25720
|
} catch (err) {
|
|
25505
25721
|
toast('Failed: ' + err, 'error');
|
|
25506
25722
|
}
|
|
25507
25723
|
}
|
|
25508
25724
|
|
|
25509
|
-
// Bulk migrator
|
|
25510
|
-
// that has
|
|
25725
|
+
// 1.18.156 — Bulk migrator. Default behavior auto-creates a folder-form
|
|
25726
|
+
// skill for any cron that has no matching skill (createMissing=true),
|
|
25727
|
+
// because the user's 15 legacy crons typically have no match — without
|
|
25728
|
+
// auto-create, the bulk button silently skips everything. .bak backups
|
|
25729
|
+
// preserve every original CRON.md so rollback is one cp away.
|
|
25511
25730
|
async function migrateAllCronsToSkills() {
|
|
25512
|
-
if (!confirm('Convert ALL
|
|
25513
|
-
'
|
|
25514
|
-
'
|
|
25515
|
-
'no
|
|
25731
|
+
if (!confirm('Convert ALL legacy crons to scheduled skills?\\n\\n' +
|
|
25732
|
+
'For each cron:\\n' +
|
|
25733
|
+
'• If a matching skill exists → bind to it\\n' +
|
|
25734
|
+
'• If no matching skill → auto-create one (kebab-case, Anthropic format) from the cron prompt\\n\\n' +
|
|
25735
|
+
'CRON.md files get .bak backups. Refine generated skills in the Builder afterwards.')) return;
|
|
25516
25736
|
try {
|
|
25517
|
-
var r = await apiFetch('/api/cron/migrate-all-to-skills', {
|
|
25737
|
+
var r = await apiFetch('/api/cron/migrate-all-to-skills', {
|
|
25738
|
+
method: 'POST',
|
|
25739
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25740
|
+
body: JSON.stringify({ createMissing: true }),
|
|
25741
|
+
});
|
|
25518
25742
|
var d = await r.json();
|
|
25519
25743
|
if (!r.ok) { toast(d.error || 'Bulk migration failed', 'error'); return; }
|
|
25520
|
-
var migrated =
|
|
25744
|
+
var migrated = d.migrated || [];
|
|
25745
|
+
var created = migrated.filter(function(m) { return m.created; }).length;
|
|
25746
|
+
var bound = migrated.length - created;
|
|
25521
25747
|
var skipped = (d.skipped || []).length;
|
|
25522
|
-
var
|
|
25748
|
+
var parts = [];
|
|
25749
|
+
if (bound > 0) parts.push(bound + ' bound to existing skill' + (bound === 1 ? '' : 's'));
|
|
25750
|
+
if (created > 0) parts.push(created + ' new skill' + (created === 1 ? '' : 's') + ' created');
|
|
25751
|
+
var msg = 'Migrated ' + migrated.length + ' cron' + (migrated.length === 1 ? '' : 's')
|
|
25752
|
+
+ (parts.length ? ' (' + parts.join(', ') + ')' : '');
|
|
25523
25753
|
if (skipped > 0) msg += '. ' + skipped + ' skipped — see console for reasons.';
|
|
25524
|
-
toast(msg, migrated > 0 ? 'success' : 'info');
|
|
25754
|
+
toast(msg, migrated.length > 0 ? 'success' : 'info');
|
|
25525
25755
|
if (skipped > 0 && d.skipped) console.warn('[migrate-all-to-skills] skipped:', d.skipped);
|
|
25526
25756
|
if (typeof refreshCron === 'function') refreshCron();
|
|
25757
|
+
if (typeof refreshSkills === 'function') refreshSkills();
|
|
25527
25758
|
} catch (err) {
|
|
25528
25759
|
toast('Failed: ' + err, 'error');
|
|
25529
25760
|
}
|
|
@@ -25670,7 +25901,13 @@ function renderRecentHistoryList(runs) {
|
|
|
25670
25901
|
var msg = String(entry.error).slice(0, 120);
|
|
25671
25902
|
errorPreview = '<div style="font-size:11px;color:var(--red);margin-top:2px;word-break:break-word">' + esc(msg) + '</div>';
|
|
25672
25903
|
} else if (entry.outputPreview) {
|
|
25673
|
-
|
|
25904
|
+
// 1.18.155 — strip the __NOTHING__ sentinel agents emit when they
|
|
25905
|
+
// intentionally stay silent (it's the "no signal" contract from
|
|
25906
|
+
// run-agent-cron.ts howToRespond block, not output the user should see).
|
|
25907
|
+
var rawPreview = String(entry.outputPreview);
|
|
25908
|
+
var preview = (rawPreview === '__NOTHING__' || rawPreview.trim() === '__NOTHING__')
|
|
25909
|
+
? '(no output — agent reported nothing worth saying)'
|
|
25910
|
+
: rawPreview.slice(0, 140);
|
|
25674
25911
|
errorPreview = '<div style="font-size:11px;color:var(--text-muted);margin-top:2px;word-break:break-word">' + esc(preview) + '</div>';
|
|
25675
25912
|
}
|
|
25676
25913
|
// PRD Phase 1.1: goal cell. Empty cell when no goal configured (status='skipped'
|
|
@@ -25775,16 +26012,31 @@ async function refreshHealthStrip() {
|
|
|
25775
26012
|
var ops = await apiFetch('/api/build/operations?hours=1&limit=10').then(function(rr) { return rr.json(); });
|
|
25776
26013
|
activeRuns = ((ops && ops.runningNow) || []).length;
|
|
25777
26014
|
} catch (e) { /* fall back to 0 */ }
|
|
25778
|
-
// Top failure category for the day.
|
|
26015
|
+
// Top failure category for the day. 1.18.155 — when no failureCategory
|
|
26016
|
+
// is set on the failed runs (which is common for raw SDK errors like
|
|
26017
|
+
// "Prompt is too long"), fall back to grouping by jobName so the user
|
|
26018
|
+
// sees WHICH job is failing instead of a useless dash. Categories still
|
|
26019
|
+
// win when present (more semantic than a job name).
|
|
25779
26020
|
var catCounts = {};
|
|
26021
|
+
var jobCounts = {};
|
|
25780
26022
|
for (var i = 0; i < last24.length; i++) {
|
|
25781
26023
|
var c = last24[i].failureCategory;
|
|
25782
26024
|
if (c) catCounts[c] = (catCounts[c] || 0) + 1;
|
|
26025
|
+
if (last24[i].status === 'error' || last24[i].status === 'failed') {
|
|
26026
|
+
var jn = last24[i].jobName || last24[i].name;
|
|
26027
|
+
if (jn) jobCounts[jn] = (jobCounts[jn] || 0) + 1;
|
|
26028
|
+
}
|
|
25783
26029
|
}
|
|
25784
26030
|
var topCat = null, topCount = 0;
|
|
25785
26031
|
Object.keys(catCounts).forEach(function(k) {
|
|
25786
26032
|
if (catCounts[k] > topCount) { topCat = k; topCount = catCounts[k]; }
|
|
25787
26033
|
});
|
|
26034
|
+
var topJob = null, topJobCount = 0;
|
|
26035
|
+
Object.keys(jobCounts).forEach(function(k) {
|
|
26036
|
+
if (jobCounts[k] > topJobCount) { topJob = k; topJobCount = jobCounts[k]; }
|
|
26037
|
+
});
|
|
26038
|
+
// Prefer the categorized one (more semantic), fall back to job name.
|
|
26039
|
+
if (!topCat && topJob) { topCat = topJob; topCount = topJobCount; }
|
|
25788
26040
|
// Render six tiles.
|
|
25789
26041
|
function tile(label, value, sub, color) {
|
|
25790
26042
|
return '<div class="health-tile">'
|
|
@@ -26450,7 +26702,12 @@ function renderRunListBody(allRuns) {
|
|
|
26450
26702
|
if (status === 'error' && entry.error) {
|
|
26451
26703
|
preview = '<div style="font-size:11px;color:var(--red);margin-top:2px;word-break:break-word">' + esc(String(entry.error).slice(0, 140)) + '</div>';
|
|
26452
26704
|
} else if (entry.outputPreview) {
|
|
26453
|
-
|
|
26705
|
+
// 1.18.155 — see renderRecentHistoryList for the same __NOTHING__ strip.
|
|
26706
|
+
var rawOp = String(entry.outputPreview);
|
|
26707
|
+
var opText = (rawOp === '__NOTHING__' || rawOp.trim() === '__NOTHING__')
|
|
26708
|
+
? '(no output — agent reported nothing worth saying)'
|
|
26709
|
+
: rawOp.slice(0, 120);
|
|
26710
|
+
preview = '<div style="font-size:11px;color:var(--text-muted);margin-top:2px;word-break:break-word">' + esc(opText) + '</div>';
|
|
26454
26711
|
}
|
|
26455
26712
|
// 1.18.89: cost label. Showing 4 decimals for sub-penny costs (Haiku
|
|
26456
26713
|
// runs land in fractions of a cent), 2 decimals when ≥ $0.01.
|
|
@@ -27079,7 +27336,11 @@ async function refreshCron() {
|
|
|
27079
27336
|
var bannerHtml = '';
|
|
27080
27337
|
var dismissed = localStorage.getItem('clem-skill-migrate-banner-dismissed') === '1';
|
|
27081
27338
|
if (legacyCount > 0 && !dismissed) {
|
|
27082
|
-
|
|
27339
|
+
// 1.18.155 — data-banner-kind tags this as the legacy-cron soft-
|
|
27340
|
+
// deprecation banner so refreshCronMigrateBanner can suppress its
|
|
27341
|
+
// secondary "clean up preambles" banner when this one is showing
|
|
27342
|
+
// (full migration is a superset; showing both is confusing noise).
|
|
27343
|
+
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">'
|
|
27083
27344
|
+ '<span style="font-size:18px">⚡</span>'
|
|
27084
27345
|
+ '<div style="flex:1;min-width:200px">'
|
|
27085
27346
|
+ '<div style="font-size:13px;font-weight:500;color:var(--text-primary)">' + legacyCount + ' legacy cron task' + (legacyCount === 1 ? '' : 's') + ' can become scheduled skills</div>'
|
|
@@ -29049,6 +29310,19 @@ async function refreshCronMigrateBanner() {
|
|
|
29049
29310
|
return;
|
|
29050
29311
|
}
|
|
29051
29312
|
_cronMigratePreview = d;
|
|
29313
|
+
// 1.18.155 — suppress this banner when the upstream "X legacy crons
|
|
29314
|
+
// can become scheduled skills" banner is already showing. The full
|
|
29315
|
+
// migration there is a superset of this cleanup (the destination skill
|
|
29316
|
+
// gets a clean prompt automatically), so showing both at once is just
|
|
29317
|
+
// confusing noise. If the upstream banner is dismissed via localStorage
|
|
29318
|
+
// OR there are no legacy crons to migrate, this orange banner shows up
|
|
29319
|
+
// as the secondary action.
|
|
29320
|
+
var dismissed = localStorage.getItem('clem-skill-migrate-banner-dismissed') === '1';
|
|
29321
|
+
var hasLegacyCronBanner = !dismissed && document.querySelector('[data-banner-kind="legacy-cron-soft-deprecation"]');
|
|
29322
|
+
if (hasLegacyCronBanner) {
|
|
29323
|
+
host.innerHTML = '';
|
|
29324
|
+
return;
|
|
29325
|
+
}
|
|
29052
29326
|
var n = d.eligible.length;
|
|
29053
29327
|
host.innerHTML =
|
|
29054
29328
|
'<div style="margin:0 0 14px;padding:12px 16px;border:1px solid var(--accent);background:rgba(255,141,0,0.06);border-radius:8px;display:flex;align-items:center;gap:14px;flex-wrap:wrap">'
|
|
@@ -101,6 +101,10 @@ export interface ScheduledTaskCard {
|
|
|
101
101
|
* team comms / auto-matched skills. The visibility-on-card flag for
|
|
102
102
|
* "this trick will run with only what you see here." */
|
|
103
103
|
predictable?: boolean;
|
|
104
|
+
/** 1.18.154/155 — Lean envelope (meta-jobs). Surfaced in the row badge
|
|
105
|
+
* so the user understands why insight-check / outcome-grader / etc.
|
|
106
|
+
* show a tiny prompt + restricted MCP catalog. */
|
|
107
|
+
lean?: boolean;
|
|
104
108
|
}
|
|
105
109
|
export interface ScheduledWorkflowCard {
|
|
106
110
|
type: 'scheduled_workflow';
|
|
@@ -216,6 +216,7 @@ export function buildOperationsSnapshot(input) {
|
|
|
216
216
|
tags: asStringArray(job.tags),
|
|
217
217
|
category: typeof job.category === 'string' && job.category.trim() ? job.category.trim() : undefined,
|
|
218
218
|
predictable: typeof job.predictable === 'boolean' ? job.predictable : undefined,
|
|
219
|
+
lean: typeof job.lean === 'boolean' ? job.lean : undefined,
|
|
219
220
|
};
|
|
220
221
|
}).sort((a, b) => a.owner.localeCompare(b.owner) || a.displayName.localeCompare(b.displayName));
|
|
221
222
|
const scheduledWorkflows = input.workflowSummaries
|