clementine-agent 1.18.170 → 1.18.172

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.
@@ -113,6 +113,14 @@ export function markRunning(id, opts) {
113
113
  safeWrite(pathFor(id, opts), task);
114
114
  return task;
115
115
  }
116
+ function writeFullResultFile(id, result, opts) {
117
+ if (result.length <= RESULT_TRUNCATE_BYTES)
118
+ return undefined;
119
+ const file = path.join(dirFor(opts), `${id}.result.md`);
120
+ mkdirSync(path.dirname(file), { recursive: true });
121
+ writeFileSync(file, result);
122
+ return file;
123
+ }
116
124
  /** Transition to 'done' with final result. */
117
125
  export function markDone(id, result, deliverableNote, opts) {
118
126
  const task = loadBackgroundTask(id, opts);
@@ -122,9 +130,14 @@ export function markDone(id, result, deliverableNote, opts) {
122
130
  return task;
123
131
  task.status = 'done';
124
132
  task.completedAt = new Date().toISOString();
133
+ const resultPath = writeFullResultFile(id, result, opts);
134
+ if (resultPath)
135
+ task.resultPath = resultPath;
125
136
  task.result = result;
126
137
  if (deliverableNote)
127
138
  task.deliverableNote = deliverableNote;
139
+ else if (resultPath)
140
+ task.deliverableNote = resultPath;
128
141
  safeWrite(pathFor(id, opts), task);
129
142
  return task;
130
143
  }
@@ -58,10 +58,21 @@ import pino from 'pino';
58
58
  import { searchSkills } from './skill-extractor.js';
59
59
  const logger = pino({ name: 'clementine.chat-skill-resolver' });
60
60
  // ── Tunables ──────────────────────────────────────────────────────────
61
- /** Default minimum score to consider a skill match real. Mirrors the
62
- * legacy `assistant.ts:1492` threshold. Skill auto-match is heuristic;
63
- * this filter keeps weak matches from injecting unrelated tooling. */
61
+ /** Default minimum score for user-authored skill matches. Mirrors the
62
+ * legacy `assistant.ts:1492` threshold. */
64
63
  const DEFAULT_MIN_SCORE = 4;
64
+ /** Higher threshold applied when ALL matches are auto-generated MCP-derived
65
+ * skills (no user-authored signal). 1.18.171 hotfix: a vague chat message
66
+ * ("did our changes break it?") matched three unrelated auto-skills
67
+ * (ElevenLabs + apify) at score 5.5 each because semantic-only matching
68
+ * drifted toward whatever embeddings were closest. Bumping the bar for
69
+ * auto-only match-sets keeps that noise out of the system prompt. */
70
+ const AUTO_ONLY_MIN_SCORE = 8;
71
+ /** When ALL matches are auto-generated AND they reference this many or
72
+ * more distinct servers, the cluster is treated as semantic-noise and
73
+ * the injection is skipped entirely. Three different services have no
74
+ * business being "all relevant" to a single user message. */
75
+ const AUTO_ONLY_SERVER_NOISE_THRESHOLD = 3;
65
76
  /** Default top-K matches to aggregate. Single-tool requests usually
66
77
  * return one strong match; category requests ("salesforce") return
67
78
  * several similarly-scored auto-skills. Top-3 covers both. Raising
@@ -228,9 +239,36 @@ export function resolveSkillsForChat(userMessage, opts = {}) {
228
239
  logger.debug({ err }, 'chat-skill-resolver: searchSkills failed (non-fatal)');
229
240
  return empty;
230
241
  }
231
- const matches = candidates
232
- .filter((m) => m.score >= minScore)
242
+ // 1.18.171 hotfix: detect auto-only match-sets and apply the higher
243
+ // threshold + noise-cluster filter so vague chat messages don't surface
244
+ // unrelated MCP context. See the comment block on AUTO_ONLY_MIN_SCORE.
245
+ const isAutoMatch = (m) => m.name.startsWith('auto-');
246
+ const candidatesAllAuto = candidates.length > 0 && candidates.every(isAutoMatch);
247
+ const effectiveMinScore = candidatesAllAuto
248
+ ? Math.max(minScore, AUTO_ONLY_MIN_SCORE)
249
+ : minScore;
250
+ let matches = candidates
251
+ .filter((m) => m.score >= effectiveMinScore)
233
252
  .slice(0, limit);
253
+ // Auto-only noise cluster filter: when every survivor is auto AND they
254
+ // collectively reference too many distinct servers (no semantic
255
+ // clustering on a single service), treat as drift and drop.
256
+ if (matches.length >= 2 && matches.every(isAutoMatch)) {
257
+ const seenServers = new Set();
258
+ for (const m of matches) {
259
+ for (const s of extractMcpServersFromMatch(m))
260
+ seenServers.add(s);
261
+ }
262
+ if (seenServers.size >= AUTO_ONLY_SERVER_NOISE_THRESHOLD) {
263
+ logger.info({
264
+ droppedMatches: matches.map(m => ({ name: m.name, score: Number(m.score.toFixed(2)) })),
265
+ distinctServers: [...seenServers],
266
+ reason: 'auto_only_server_cluster_too_wide',
267
+ queryChars,
268
+ }, 'chat-skill-resolver: dropped match-set (semantic noise)');
269
+ matches = [];
270
+ }
271
+ }
234
272
  if (matches.length === 0) {
235
273
  return {
236
274
  ...empty,
@@ -0,0 +1,9 @@
1
+ export interface ComplexTaskRecommendation {
2
+ score: number;
3
+ reasons: string[];
4
+ suggestedMaxMinutes: number;
5
+ plan: string[];
6
+ queueImmediately: boolean;
7
+ }
8
+ export declare function detectComplexTaskForBackground(text: string): ComplexTaskRecommendation | null;
9
+ //# sourceMappingURL=complex-task-detector.d.ts.map
@@ -0,0 +1,118 @@
1
+ const SKILL_AUTHORING_RE = /\b(create|make|build|draft|write|teach|save|update)\b.{0,40}\b(skill|SKILL\.md)\b|\bskill[- ]creator\b/i;
2
+ const EXPLICIT_BACKGROUND_RE = /\b(background|deep mode|keep working|don't stop|dont stop|autonomous|long[- ]running|run overnight|take your time)\b/i;
3
+ const COMPLEX_WORK_RE = /\b(audit|research|analy[sz]e|review|scrape|crawl|extract|enrich|compile|compare|verify|cross[- ]check|triage|reconcile|draft|generate|update|sync|report back|write back)\b/i;
4
+ const BATCH_RE = /\b(all|every|each|bulk|batch|list of|contacts?|leads?|accounts?|tasks?|tickets?|records?|rows?|pages?|repos?|projects?)\b/i;
5
+ const SIDE_EFFECT_RE = /\b(update|write|create|draft|send|post|comment|reply|upload|append|sync|mark|close|move)\b/i;
6
+ const MULTI_STEP_RE = /\b(and then|then|after that|finally|from .* to |against .* and |across|compile .* into|check .* then)\b/i;
7
+ const SYSTEM_KEYWORDS = [
8
+ 'asana',
9
+ 'salesforce',
10
+ 'google sheet',
11
+ 'google sheets',
12
+ 'sheet',
13
+ 'sheets',
14
+ 'dataforseo',
15
+ 'hubspot',
16
+ 'notion',
17
+ 'github',
18
+ 'gmail',
19
+ 'outlook',
20
+ 'slack',
21
+ 'discord',
22
+ 'website',
23
+ 'websites',
24
+ 'crm',
25
+ 'spreadsheet',
26
+ 'csv',
27
+ 'airtable',
28
+ 'linear',
29
+ 'jira',
30
+ ];
31
+ function countSystemMentions(text) {
32
+ const lower = text.toLowerCase();
33
+ let count = 0;
34
+ for (const keyword of SYSTEM_KEYWORDS) {
35
+ if (lower.includes(keyword))
36
+ count++;
37
+ }
38
+ return count;
39
+ }
40
+ function estimatedMinutes(score, systemCount) {
41
+ if (score >= 8 || systemCount >= 4)
42
+ return 90;
43
+ if (score >= 6 || systemCount >= 3)
44
+ return 60;
45
+ return 30;
46
+ }
47
+ function buildPlan(text, systemCount) {
48
+ const lower = text.toLowerCase();
49
+ const plan = [];
50
+ plan.push('Confirm the exact scope, filters, and write/send permissions before making side-effecting changes.');
51
+ if (systemCount > 0) {
52
+ plan.push('Connect to the named systems with official MCP/API/CLI tools and use the narrowest reliable query.');
53
+ }
54
+ else {
55
+ plan.push('Gather the source material with the available project, file, web, memory, or CLI tools.');
56
+ }
57
+ if (BATCH_RE.test(text)) {
58
+ plan.push('Process records in batches, track counts, and keep skipped/error records separate.');
59
+ }
60
+ if (lower.includes('enrich') || lower.includes('dataforseo')) {
61
+ plan.push('Enrich only qualified records and keep the signal used for each output row or draft.');
62
+ }
63
+ if (SIDE_EFFECT_RE.test(text)) {
64
+ plan.push('Create drafts or updates first; only send or commit irreversible changes after explicit approval.');
65
+ }
66
+ plan.push('Return a concise final report with counts, changed locations, failures, and recommended next action.');
67
+ return plan.slice(0, 6);
68
+ }
69
+ export function detectComplexTaskForBackground(text) {
70
+ const trimmed = text.trim();
71
+ if (!trimmed)
72
+ return null;
73
+ if (SKILL_AUTHORING_RE.test(trimmed))
74
+ return null;
75
+ const systemCount = countSystemMentions(trimmed);
76
+ const reasons = [];
77
+ let score = 0;
78
+ if (EXPLICIT_BACKGROUND_RE.test(trimmed)) {
79
+ score += 4;
80
+ reasons.push('explicit background/deep-work wording');
81
+ }
82
+ if (COMPLEX_WORK_RE.test(trimmed)) {
83
+ score += 2;
84
+ reasons.push('multi-step work verb');
85
+ }
86
+ if (BATCH_RE.test(trimmed)) {
87
+ score += 2;
88
+ reasons.push('batch or many-record scope');
89
+ }
90
+ if (SIDE_EFFECT_RE.test(trimmed)) {
91
+ score += 1;
92
+ reasons.push('write/draft/update side effects');
93
+ }
94
+ if (MULTI_STEP_RE.test(trimmed)) {
95
+ score += 1;
96
+ reasons.push('multi-step sequencing');
97
+ }
98
+ if (systemCount >= 2) {
99
+ score += Math.min(4, systemCount);
100
+ reasons.push(`${systemCount} named systems or data surfaces`);
101
+ }
102
+ if (trimmed.length > 450) {
103
+ score += 1;
104
+ reasons.push('long detailed request');
105
+ }
106
+ const queueImmediately = EXPLICIT_BACKGROUND_RE.test(trimmed) && score >= 5;
107
+ const shouldOffer = queueImmediately || score >= 5 || (systemCount >= 2 && (BATCH_RE.test(trimmed) || SIDE_EFFECT_RE.test(trimmed)));
108
+ if (!shouldOffer)
109
+ return null;
110
+ return {
111
+ score,
112
+ reasons,
113
+ suggestedMaxMinutes: estimatedMinutes(score, systemCount),
114
+ plan: buildPlan(trimmed, systemCount),
115
+ queueImmediately,
116
+ };
117
+ }
118
+ //# sourceMappingURL=complex-task-detector.js.map
@@ -2936,6 +2936,40 @@ export async function cmdDashboard(opts) {
2936
2936
  res.status(500).json({ error: String(err).slice(0, 200) });
2937
2937
  }
2938
2938
  });
2939
+ app.post('/api/background-offers/:id/accept', async (req, res) => {
2940
+ try {
2941
+ const id = req.params.id;
2942
+ if (!/^bo-[a-z0-9]+-[a-z0-9]{3,10}$/.test(id)) {
2943
+ res.status(400).json({ error: 'Invalid background offer id' });
2944
+ return;
2945
+ }
2946
+ const gateway = await getGateway();
2947
+ const result = gateway.acceptBackgroundOffer('dashboard:web', id);
2948
+ if (!result.ok) {
2949
+ res.status(404).json({ ok: false, error: result.response });
2950
+ return;
2951
+ }
2952
+ res.json(result);
2953
+ }
2954
+ catch (err) {
2955
+ res.status(500).json({ error: String(err).slice(0, 200) });
2956
+ }
2957
+ });
2958
+ app.post('/api/background-offers/:id/dismiss', async (req, res) => {
2959
+ try {
2960
+ const id = req.params.id;
2961
+ if (!/^bo-[a-z0-9]+-[a-z0-9]{3,10}$/.test(id)) {
2962
+ res.status(400).json({ error: 'Invalid background offer id' });
2963
+ return;
2964
+ }
2965
+ const gateway = await getGateway();
2966
+ const result = gateway.dismissBackgroundOffer('dashboard:web', id);
2967
+ res.status(result.ok ? 200 : 404).json(result.ok ? result : { ok: false, error: result.response });
2968
+ }
2969
+ catch (err) {
2970
+ res.status(500).json({ error: String(err).slice(0, 200) });
2971
+ }
2972
+ });
2939
2973
  app.get('/api/heartbeat/agent/:slug', (req, res) => {
2940
2974
  const slug = req.params.slug;
2941
2975
  const state = getHeartbeat();
@@ -21604,40 +21638,54 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
21604
21638
  </div>
21605
21639
  </div>
21606
21640
  <div style="display:flex;align-items:center;gap:8px;flex-shrink:0">
21607
- <button class="btn-secondary" onclick="openSkillStudio()" style="font-size:13px;padding:8px 14px;border-radius:6px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px" title="Open the conversational Skill Studio">
21641
+ <button class="btn-secondary" onclick="openSkillStudio()" style="font-size:13px;padding:8px 14px;border-radius:6px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px" title="Open the natural-language Skill Studio">
21608
21642
  Open Studio
21609
21643
  </button>
21610
- <button class="btn-primary" onclick="openCreateSkillModalFromComposer()" style="font-size:13px;padding:8px 14px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px" title="Open a clean blank skill editor">
21611
- Blank skill
21644
+ <button class="btn-primary" onclick="openCreateSkillModalFromComposer()" style="font-size:13px;padding:8px 14px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px" title="Open a blank SKILL.md editor">
21645
+ Manual editor
21612
21646
  </button>
21613
21647
  </div>
21614
21648
  </div>
21649
+ <div id="skill-composer-home">
21615
21650
  <div id="skill-composer" style="margin:0 0 16px;border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);padding:14px 16px">
21616
- <div style="display:grid;grid-template-columns:minmax(280px,1.2fr) minmax(260px,0.8fr);gap:14px;align-items:start">
21651
+ <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:14px;margin-bottom:12px">
21652
+ <div style="min-width:0">
21653
+ <div style="font-size:14px;font-weight:600;color:var(--text-primary);margin-bottom:3px">Skill Studio</div>
21654
+ <div style="font-size:12px;color:var(--text-muted);line-height:1.45">Describe reusable work in plain language. Optional starting points only seed the draft; nothing runs until you save or test it.</div>
21655
+ </div>
21656
+ <div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;white-space:nowrap;margin-top:2px">Natural language first</div>
21657
+ </div>
21658
+ <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px;align-items:start">
21617
21659
  <div>
21618
- <label for="skill-composer-text" style="display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:6px">Describe the skill</label>
21619
- <textarea id="skill-composer-text" rows="4" oninput="updateSkillComposerDraftState()" placeholder="Review my Asana tasks, update a Google Sheet, verify four source systems, then report back in Asana." style="width:100%;box-sizing:border-box;padding:10px 12px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-primary);font-size:13px;line-height:1.45;resize:vertical;min-height:92px"></textarea>
21660
+ <label for="skill-composer-text" style="display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:6px">What should Clementine learn?</label>
21661
+ <textarea id="skill-composer-text" rows="5" oninput="updateSkillComposerDraftState()" placeholder="Find Salesforce contacts I have not touched in 15 days, enrich the accounts with DataForSEO signals, draft cold prospecting emails, then report the drafts back for review." style="width:100%;box-sizing:border-box;padding:10px 12px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-primary);font-size:13px;line-height:1.45;resize:vertical;min-height:112px"></textarea>
21662
+ <div style="margin-top:8px;font-size:11px;color:var(--text-muted);line-height:1.45">Good skills name the repeatable outcome, required tools or data, approval boundaries, and what counts as done.</div>
21620
21663
  </div>
21621
21664
  <div>
21622
- <div style="font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:6px">Starting point</div>
21665
+ <div style="font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:6px">Optional starting points</div>
21623
21666
  <div id="skill-composer-modes" style="display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:4px;margin-bottom:10px">
21624
21667
  <button type="button" class="skill-composer-mode" data-kind="outcome" onclick="setSkillComposerMode('outcome')" style="padding:7px 6px;border:1px solid var(--accent);border-radius:6px;background:rgba(255,141,0,0.10);color:var(--accent);font-size:11px;font-weight:600;cursor:pointer">Outcome</button>
21625
- <button type="button" class="skill-composer-mode" data-kind="tool" onclick="setSkillComposerMode('tool')" style="padding:7px 6px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-secondary);font-size:11px;font-weight:600;cursor:pointer">Tool/MCP</button>
21668
+ <button type="button" class="skill-composer-mode" data-kind="tool" onclick="setSkillComposerMode('tool')" style="padding:7px 6px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-secondary);font-size:11px;font-weight:600;cursor:pointer">MCP/API</button>
21626
21669
  <button type="button" class="skill-composer-mode" data-kind="cli" onclick="setSkillComposerMode('cli')" style="padding:7px 6px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-secondary);font-size:11px;font-weight:600;cursor:pointer">CLI</button>
21627
21670
  <button type="button" class="skill-composer-mode" data-kind="project" onclick="setSkillComposerMode('project')" style="padding:7px 6px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-secondary);font-size:11px;font-weight:600;cursor:pointer">Project</button>
21628
21671
  <button type="button" class="skill-composer-mode" data-kind="memory" onclick="setSkillComposerMode('memory')" style="padding:7px 6px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-secondary);font-size:11px;font-weight:600;cursor:pointer">Memory</button>
21629
21672
  </div>
21630
21673
  <label id="skill-composer-anchor-label" for="skill-composer-anchor" style="display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:6px">Anchor</label>
21631
- <input id="skill-composer-anchor" list="skill-composer-anchor-options" oninput="updateSkillComposerDraftState()" placeholder="Optional tool, project, command, or memory source" style="width:100%;box-sizing:border-box;padding:9px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-primary);font-size:12px">
21674
+ <div style="display:flex;gap:6px;align-items:center">
21675
+ <input id="skill-composer-anchor" list="skill-composer-anchor-options" oninput="updateSkillComposerDraftState()" onkeydown="if(event.key==='Enter'){event.preventDefault();addSkillComposerDependency();}" placeholder="Optional tool, project, command, or memory source" style="flex:1;min-width:0;box-sizing:border-box;padding:9px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-primary);font-size:12px">
21676
+ <button type="button" class="btn-secondary" onclick="addSkillComposerDependency()" style="font-size:12px;padding:8px 10px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text-primary);cursor:pointer">Add</button>
21677
+ </div>
21632
21678
  <datalist id="skill-composer-anchor-options"></datalist>
21633
21679
  <div id="skill-composer-anchor-summary" style="margin-top:8px;min-height:22px;font-size:11px;color:var(--text-muted);line-height:1.45">No starting point selected.</div>
21680
+ <div id="skill-composer-preview" style="margin-top:10px;max-height:180px;overflow:auto;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);padding:10px 12px;font-size:11px;line-height:1.45;color:var(--text-secondary)"></div>
21634
21681
  <div style="display:flex;align-items:center;justify-content:flex-end;gap:8px;margin-top:12px">
21635
- <button type="button" class="btn-secondary" onclick="openSkillStudio()" style="font-size:12px;padding:7px 12px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text-primary);cursor:pointer">Open Studio</button>
21636
- <button type="button" class="btn-primary" id="skill-composer-draft-btn" onclick="startSkillComposerDraft()" disabled style="font-size:12px;padding:7px 14px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-weight:600;cursor:pointer;opacity:0.55">Draft skill</button>
21682
+ <button type="button" class="btn-secondary" onclick="startSkillComposerChat()" style="font-size:12px;padding:7px 12px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text-primary);cursor:pointer">Build in chat</button>
21683
+ <button type="button" class="btn-primary" id="skill-composer-draft-btn" onclick="startSkillComposerDraft()" disabled style="font-size:12px;padding:7px 14px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-weight:600;cursor:pointer;opacity:0.55">Review draft</button>
21637
21684
  </div>
21638
21685
  </div>
21639
21686
  </div>
21640
21687
  </div>
21688
+ </div>
21641
21689
  <div style="display:grid;grid-template-columns:380px 1fr;gap:18px;height:calc(100vh - 360px);min-height:440px">
21642
21690
  <div id="skills-list-pane" style="overflow-y:auto;border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary)">
21643
21691
  <div style="padding:14px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px">
@@ -28725,6 +28773,7 @@ var _skillsState = {
28725
28773
 
28726
28774
  var _skillComposerMode = 'outcome';
28727
28775
  var _skillComposerOptionsCache = null;
28776
+ var _skillComposerDependencies = [];
28728
28777
 
28729
28778
  var _skillComposerCopy = {
28730
28779
  outcome: {
@@ -28785,34 +28834,77 @@ function updateSkillComposerDraftState() {
28785
28834
  var text = (document.getElementById('skill-composer-text') || {}).value || '';
28786
28835
  var anchor = (document.getElementById('skill-composer-anchor') || {}).value || '';
28787
28836
  var btn = document.getElementById('skill-composer-draft-btn');
28788
- var enabled = !!(text.trim() || anchor.trim());
28837
+ var enabled = !!(text.trim() || anchor.trim() || _skillComposerDependencies.length);
28789
28838
  if (btn) {
28790
28839
  btn.disabled = !enabled;
28791
28840
  btn.style.opacity = enabled ? '1' : '0.55';
28792
28841
  btn.style.cursor = enabled ? 'pointer' : 'not-allowed';
28793
28842
  }
28794
28843
  renderSkillComposerAnchorSummary(text, anchor);
28844
+ renderSkillComposerPreview();
28795
28845
  }
28796
28846
 
28797
28847
  function renderSkillComposerAnchorSummary(text, anchor) {
28798
28848
  var box = document.getElementById('skill-composer-anchor-summary');
28799
28849
  if (!box) return;
28800
28850
  var trimmed = String(anchor || '').trim();
28801
- if (!trimmed) {
28851
+ var deps = getSkillComposerDependencies(false);
28852
+ if (!trimmed && deps.length === 0) {
28802
28853
  box.innerHTML = '<span style="color:var(--text-muted)">No starting point selected.</span>';
28803
28854
  return;
28804
28855
  }
28805
- var modeCopy = _skillComposerCopy[_skillComposerMode] || _skillComposerCopy.outcome;
28806
- var tools = inferSkillComposerTools(_skillComposerMode, trimmed);
28807
- var toolText = tools.length
28808
- ? tools.map(function(t) { return '<code style="font-size:10px;background:var(--bg-primary);border:1px solid var(--border);padding:1px 5px;border-radius:4px;color:var(--text-primary)">' + esc(t) + '</code>'; }).join(' ')
28809
- : '<span style="color:var(--text-muted)">no explicit tool allowlist</span>';
28810
- box.innerHTML = '<div style="display:flex;align-items:flex-start;gap:6px;flex-wrap:wrap">'
28811
- + '<span style="color:var(--accent);font-weight:600">' + esc(modeCopy.promptLabel) + ':</span>'
28812
- + '<code style="font-size:10px;background:var(--bg-primary);border:1px solid var(--border);padding:1px 5px;border-radius:4px;color:var(--text-primary);max-width:100%;overflow:hidden;text-overflow:ellipsis">' + esc(trimmed) + '</code>'
28813
- + '<span style="color:var(--text-muted)">will seed</span>'
28814
- + toolText
28815
- + '</div>';
28856
+ var html = '<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">';
28857
+ for (var i = 0; i < deps.length; i++) {
28858
+ var dep = deps[i];
28859
+ var copy = _skillComposerCopy[dep.mode] || _skillComposerCopy.outcome;
28860
+ html += '<span style="display:inline-flex;align-items:center;gap:5px;max-width:100%;padding:2px 7px;border:1px solid var(--border);border-radius:999px;background:var(--bg-primary);color:var(--text-primary);font-size:10px">'
28861
+ + '<strong style="color:var(--accent);font-weight:600">' + esc(copy.promptLabel) + '</strong>'
28862
+ + '<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:190px">' + esc(dep.value) + '</span>'
28863
+ + '<button type="button" onclick="removeSkillComposerDependency(' + i + ')" title="Remove" style="border:none;background:transparent;color:var(--text-muted);cursor:pointer;padding:0;font-size:12px;line-height:1">&times;</button>'
28864
+ + '</span>';
28865
+ }
28866
+ if (trimmed) {
28867
+ var modeCopy = _skillComposerCopy[_skillComposerMode] || _skillComposerCopy.outcome;
28868
+ var tools = inferSkillComposerTools(_skillComposerMode, trimmed);
28869
+ html += '<span style="color:var(--text-muted)">Pending ' + esc(modeCopy.promptLabel) + ':</span>'
28870
+ + '<code style="font-size:10px;background:var(--bg-primary);border:1px solid var(--border);padding:1px 5px;border-radius:4px;color:var(--text-primary);max-width:100%;overflow:hidden;text-overflow:ellipsis">' + esc(trimmed) + '</code>'
28871
+ + (tools.length ? '<span style="color:var(--text-muted)">adds ' + esc(tools.join(', ')) + '</span>' : '');
28872
+ }
28873
+ html += '</div>';
28874
+ box.innerHTML = html;
28875
+ }
28876
+
28877
+ function getSkillComposerDependencies(includePending) {
28878
+ var deps = _skillComposerDependencies.slice();
28879
+ if (includePending) {
28880
+ var pending = ((document.getElementById('skill-composer-anchor') || {}).value || '').trim();
28881
+ if (pending) deps.push({ mode: _skillComposerMode || 'outcome', value: pending });
28882
+ }
28883
+ var seen = {};
28884
+ return deps.filter(function(dep) {
28885
+ var key = dep.mode + ':' + dep.value;
28886
+ if (seen[key]) return false;
28887
+ seen[key] = true;
28888
+ return true;
28889
+ });
28890
+ }
28891
+
28892
+ function addSkillComposerDependency() {
28893
+ var input = document.getElementById('skill-composer-anchor');
28894
+ var value = (input && input.value || '').trim();
28895
+ if (!value) return;
28896
+ var mode = _skillComposerMode || 'outcome';
28897
+ var key = mode + ':' + value;
28898
+ var exists = _skillComposerDependencies.some(function(dep) { return dep.mode + ':' + dep.value === key; });
28899
+ if (!exists) _skillComposerDependencies.push({ mode: mode, value: value });
28900
+ if (input) input.value = '';
28901
+ updateSkillComposerDraftState();
28902
+ }
28903
+
28904
+ function removeSkillComposerDependency(index) {
28905
+ if (index < 0 || index >= _skillComposerDependencies.length) return;
28906
+ _skillComposerDependencies.splice(index, 1);
28907
+ updateSkillComposerDraftState();
28816
28908
  }
28817
28909
 
28818
28910
  function inferSkillComposerTools(mode, anchor) {
@@ -28888,12 +28980,19 @@ function slugifySkillTitle(title) {
28888
28980
 
28889
28981
  function buildSkillComposerDraftSeed() {
28890
28982
  var text = ((document.getElementById('skill-composer-text') || {}).value || '').trim();
28891
- var anchor = ((document.getElementById('skill-composer-anchor') || {}).value || '').trim();
28892
28983
  var mode = _skillComposerMode || 'outcome';
28893
28984
  var modeCopy = _skillComposerCopy[mode] || _skillComposerCopy.outcome;
28985
+ var deps = getSkillComposerDependencies(true);
28986
+ var anchor = deps.map(function(dep) {
28987
+ var copy = _skillComposerCopy[dep.mode] || _skillComposerCopy.outcome;
28988
+ return copy.promptLabel + ': ' + dep.value;
28989
+ }).join('; ');
28894
28990
  var title = inferSkillComposerTitle(text, anchor);
28895
28991
  var name = slugifySkillTitle(title);
28896
- var tools = inferSkillComposerTools(mode, anchor);
28992
+ var tools = [];
28993
+ for (var di = 0; di < deps.length; di++) {
28994
+ tools = tools.concat(inferSkillComposerTools(deps[di].mode, deps[di].value));
28995
+ }
28897
28996
  var hay = (text + ' ' + anchor).toLowerCase();
28898
28997
  if (hay.indexOf('salesforce') !== -1) tools = dedupeSkillComposerToolNames(tools.concat(['Bash']));
28899
28998
  if (hay.indexOf('dataforseo') !== -1 && tools.indexOf('mcp__dataforseo__*') === -1) tools.push('mcp__dataforseo__*');
@@ -28909,13 +29008,18 @@ function buildSkillComposerDraftSeed() {
28909
29008
  if (description.length > 1024) description = description.slice(0, 1024);
28910
29009
 
28911
29010
  var toolLines = tools.length ? tools.map(function(t) { return '- ' + t; }).join('\\n') : '- No explicit allowlist yet; add only the tools this skill truly needs.';
28912
- var anchorLine = anchor ? modeCopy.promptLabel + ': ' + anchor : 'None selected';
29011
+ var anchorLine = deps.length
29012
+ ? deps.map(function(dep) {
29013
+ var copy = _skillComposerCopy[dep.mode] || _skillComposerCopy.outcome;
29014
+ return '- ' + copy.promptLabel + ': ' + dep.value;
29015
+ }).join('\\n')
29016
+ : '- None selected';
28913
29017
  var body = '# ' + title + '\\n\\n'
28914
29018
  + '## Use When\\n\\n'
28915
29019
  + description + '\\n\\n'
28916
29020
  + '## Starting Point\\n\\n'
28917
29021
  + '- Outcome: ' + (text || 'Fill in the exact user goal before saving.') + '\\n'
28918
- + '- ' + anchorLine + '\\n\\n'
29022
+ + anchorLine + '\\n\\n'
28919
29023
  + '## Required Tools\\n\\n'
28920
29024
  + toolLines + '\\n\\n'
28921
29025
  + '## Procedure\\n\\n'
@@ -28961,10 +29065,37 @@ function buildSkillComposerDraftSeed() {
28961
29065
  description: description,
28962
29066
  body: body,
28963
29067
  tools: tools,
28964
- note: anchor ? ('Seeded from ' + modeCopy.promptLabel + ': ' + anchor) : 'Seeded from the natural language description.',
29068
+ note: deps.length ? ('Seeded from ' + deps.length + ' starting point' + (deps.length === 1 ? '' : 's') + '.') : 'Seeded from the natural language description.',
28965
29069
  };
28966
29070
  }
28967
29071
 
29072
+ function renderSkillComposerPreview() {
29073
+ var box = document.getElementById('skill-composer-preview');
29074
+ if (!box) return;
29075
+ var text = ((document.getElementById('skill-composer-text') || {}).value || '').trim();
29076
+ var deps = getSkillComposerDependencies(true);
29077
+ if (!text && deps.length === 0) {
29078
+ box.innerHTML = '<span style="color:var(--text-muted)">Preview appears here as you describe the skill or add tools.</span>';
29079
+ return;
29080
+ }
29081
+ var seed = buildSkillComposerDraftSeed();
29082
+ var depHtml = deps.length
29083
+ ? deps.map(function(dep) {
29084
+ var copy = _skillComposerCopy[dep.mode] || _skillComposerCopy.outcome;
29085
+ return '<span style="display:inline-flex;margin:0 4px 4px 0;padding:1px 6px;border:1px solid var(--border);border-radius:999px;background:var(--bg-secondary);font-size:10px"><strong style="color:var(--accent);margin-right:4px">' + esc(copy.promptLabel) + '</strong>' + esc(dep.value) + '</span>';
29086
+ }).join('')
29087
+ : '<span style="color:var(--text-muted)">No explicit dependencies yet.</span>';
29088
+ var toolsHtml = seed.tools.length
29089
+ ? seed.tools.map(function(t) { return '<code style="font-size:10px;background:var(--bg-secondary);border:1px solid var(--border);padding:1px 5px;border-radius:4px;color:var(--text-primary);margin-right:4px">' + esc(t) + '</code>'; }).join('')
29090
+ : '<span style="color:var(--text-muted)">inherits default runtime surface</span>';
29091
+ box.innerHTML =
29092
+ '<div style="font-weight:600;color:var(--text-primary);margin-bottom:4px">' + esc(seed.title) + '</div>' +
29093
+ '<div style="color:var(--text-muted);margin-bottom:8px">' + esc(seed.name) + '</div>' +
29094
+ '<div style="margin-bottom:8px"><strong style="color:var(--text-secondary)">Dependencies</strong><br>' + depHtml + '</div>' +
29095
+ '<div style="margin-bottom:8px"><strong style="color:var(--text-secondary)">Tools</strong><br>' + toolsHtml + '</div>' +
29096
+ '<div><strong style="color:var(--text-secondary)">Trigger description</strong><br>' + esc(seed.description.slice(0, 260)) + (seed.description.length > 260 ? '...' : '') + '</div>';
29097
+ }
29098
+
28968
29099
  async function hydrateSkillComposerOptions() {
28969
29100
  if (_skillComposerOptionsCache) return _skillComposerOptionsCache;
28970
29101
  var cache = { tool: [], cli: [], project: [], memory: [] };
@@ -29062,15 +29193,21 @@ async function renderSkillComposerOptions() {
29062
29193
 
29063
29194
  function buildSkillComposerPrompt() {
29064
29195
  var text = ((document.getElementById('skill-composer-text') || {}).value || '').trim();
29065
- var anchor = ((document.getElementById('skill-composer-anchor') || {}).value || '').trim();
29066
- var modeCopy = _skillComposerCopy[_skillComposerMode] || _skillComposerCopy.outcome;
29196
+ var deps = getSkillComposerDependencies(true);
29067
29197
  var lines = [
29068
29198
  'Use skill-creator principles to draft a Clementine skill in Skill Studio.',
29069
29199
  '',
29070
29200
  'Outcome: ' + (text || '(ask me for the outcome before drafting)'),
29071
- 'Starting point: ' + modeCopy.promptLabel,
29201
+ 'Starting points:',
29072
29202
  ];
29073
- if (anchor) lines.push('Anchor: ' + anchor);
29203
+ if (deps.length) {
29204
+ for (var i = 0; i < deps.length; i++) {
29205
+ var copy = _skillComposerCopy[deps[i].mode] || _skillComposerCopy.outcome;
29206
+ lines.push('- ' + copy.promptLabel + ': ' + deps[i].value);
29207
+ }
29208
+ } else {
29209
+ lines.push('- None selected');
29210
+ }
29074
29211
  lines.push(
29075
29212
  '',
29076
29213
  'Draft rules:',
@@ -29087,6 +29224,17 @@ function startSkillComposerDraft() {
29087
29224
  openCreateSkillModalFromComposer({ draft: true });
29088
29225
  }
29089
29226
 
29227
+ function startSkillComposerChat() {
29228
+ var prompt = buildSkillComposerPrompt();
29229
+ if (typeof askClementineWith !== 'function') {
29230
+ toast('Chat is not ready yet. Try again after the dashboard finishes loading.', 'error');
29231
+ return;
29232
+ }
29233
+ closeSkillStudio({ silent: true });
29234
+ askClementineWith(prompt, { autoSend: false });
29235
+ toast('Skill-creator prompt loaded in chat. Press send when you are ready.', 'info');
29236
+ }
29237
+
29090
29238
  function openCreateSkillModalFromComposer(opts) {
29091
29239
  opts = opts || {};
29092
29240
  var text = ((document.getElementById('skill-composer-text') || {}).value || '').trim();
@@ -29096,6 +29244,7 @@ function openCreateSkillModalFromComposer(opts) {
29096
29244
  return;
29097
29245
  }
29098
29246
  var seed = buildSkillComposerDraftSeed();
29247
+ closeSkillStudio({ silent: true });
29099
29248
  _openSkillModal({ mode: 'create', prefill: seed });
29100
29249
  if (seed.note && typeof toast === 'function') toast(seed.note, 'success');
29101
29250
  }
@@ -29876,7 +30025,7 @@ async function _openSkillModal(opts) {
29876
30025
  + '<strong style="color:var(--text-secondary);font-weight:600">Format:</strong> '
29877
30026
  + '<code style="font-size:10px;background:var(--bg-secondary);padding:1px 4px;border-radius:3px">[WHAT it does] + [WHEN to use it] + [trigger phrases]</code>'
29878
30027
  + ' &nbsp;·&nbsp; under 1024 chars · no <code style="font-size:10px">&lt; &gt;</code> · '
29879
- + '<a href="javascript:void(0)" onclick="askClementineWith(\\x27Use skill-creator to help me write a great Anthropic-canonical description for the skill I am building.\\x27)" style="color:var(--accent);text-decoration:none">use skill-creator</a>'
30028
+ + '<a href="javascript:void(0)" onclick="askSkillCreatorForDescription()" style="color:var(--accent);text-decoration:none">use skill-creator</a>'
29880
30029
  + '</div>'
29881
30030
  + '<textarea id="skill-modal-desc" rows="2" oninput="updateSkillModalCounters()" placeholder="Example: Analyzes Outlook emails and drafts triage replies. Use when user asks to triage email or mentions inbox cleanup." style="width:100%;padding:8px 10px;font-size:13px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);margin-bottom:12px;font-family:inherit;resize:vertical"></textarea>'
29882
30031
  + '<label style="display:block;font-size:12px;color:var(--text-secondary);margin-bottom:4px;font-weight:500">Allowed tools <span style="color:var(--text-muted)">(comma-separated, leave blank for default)</span></label>'
@@ -29896,7 +30045,9 @@ async function _openSkillModal(opts) {
29896
30045
  + '</div>';
29897
30046
  document.body.appendChild(modal);
29898
30047
  }
29899
- document.getElementById('skill-modal-heading').textContent = opts.mode === 'edit' ? 'Edit skill: ' + nameVal : 'New skill';
30048
+ document.getElementById('skill-modal-heading').textContent = opts.mode === 'edit'
30049
+ ? 'Edit skill: ' + nameVal
30050
+ : (prefill.note ? 'Review skill draft' : 'Manual skill editor');
29900
30051
  document.getElementById('skill-modal-original-name').value = opts.mode === 'edit' ? nameVal : '';
29901
30052
  document.getElementById('skill-modal-name').value = nameVal;
29902
30053
  document.getElementById('skill-modal-name').disabled = opts.mode === 'edit';
@@ -29917,7 +30068,8 @@ async function _openSkillModal(opts) {
29917
30068
  var errEl = document.getElementById('skill-modal-error');
29918
30069
  if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; }
29919
30070
  modal.style.display = 'flex';
29920
- document.getElementById('skill-modal-name').focus();
30071
+ var initialFocus = prefill.note ? document.getElementById('skill-modal-desc') : document.getElementById('skill-modal-name');
30072
+ if (initialFocus) initialFocus.focus();
29921
30073
  if (typeof updateSkillModalCounters === 'function') updateSkillModalCounters();
29922
30074
  if (typeof renderSkillModalToolsPreview === 'function') renderSkillModalToolsPreview();
29923
30075
  // 1.18.168 — render the compact optional template seed in create mode only.
@@ -34514,6 +34666,54 @@ function renderMd(text) {
34514
34666
  return s;
34515
34667
  }
34516
34668
 
34669
+ function extractBackgroundOfferId(text) {
34670
+ var m = String(text || '').match(/Background offer:\s*(bo-[a-z0-9]+-[a-z0-9]{3,10})/i);
34671
+ return m ? m[1] : '';
34672
+ }
34673
+
34674
+ function appendBackgroundOfferActions(bubble, text) {
34675
+ if (!bubble || bubble.querySelector('.chat-bg-offer-actions')) return;
34676
+ var id = extractBackgroundOfferId(text);
34677
+ if (!id) return;
34678
+ var panel = document.createElement('div');
34679
+ panel.className = 'chat-bg-offer-actions';
34680
+ panel.style.cssText = 'margin-top:10px;display:flex;gap:6px;flex-wrap:wrap;padding-top:8px;border-top:1px solid var(--border)';
34681
+ panel.innerHTML =
34682
+ '<button class="btn-sm btn-primary" data-bg-action="accept" style="font-size:11px;padding:5px 10px">Run in background</button>' +
34683
+ '<button class="btn-sm" data-bg-action="inline" style="font-size:11px;padding:5px 10px">Run inline anyway</button>' +
34684
+ '<button class="btn-sm" data-bg-action="skill" style="font-size:11px;padding:5px 10px">Save as skill first</button>' +
34685
+ '<button class="btn-sm" data-bg-action="dismiss" style="font-size:11px;padding:5px 10px;color:var(--text-muted)">Dismiss</button>';
34686
+ panel.querySelector('[data-bg-action="accept"]').onclick = function() { acceptBackgroundOffer(id, panel); };
34687
+ panel.querySelector('[data-bg-action="inline"]').onclick = function() { askClementineWith('run inline ' + id); };
34688
+ panel.querySelector('[data-bg-action="skill"]').onclick = function() { askClementineWith('save skill ' + id); };
34689
+ panel.querySelector('[data-bg-action="dismiss"]').onclick = function() { dismissBackgroundOffer(id, panel); };
34690
+ bubble.appendChild(panel);
34691
+ }
34692
+
34693
+ async function acceptBackgroundOffer(id, panel) {
34694
+ if (panel) panel.innerHTML = '<span style="font-size:11px;color:var(--text-muted)">Queueing background task...</span>';
34695
+ try {
34696
+ var r = await apiFetch('/api/background-offers/' + encodeURIComponent(id) + '/accept', { method: 'POST' });
34697
+ var d = await r.json();
34698
+ if (!r.ok || !d.ok) {
34699
+ if (panel) panel.innerHTML = '<span style="font-size:11px;color:var(--red)">' + esc(d.error || 'Failed to queue task') + '</span>';
34700
+ return;
34701
+ }
34702
+ if (panel) panel.innerHTML = '<div style="font-size:12px;line-height:1.45;color:var(--text-secondary)">' + renderMd(d.response || 'Background task queued.') + '</div>';
34703
+ toast('Background task queued', 'success');
34704
+ setTimeout(refreshAll, 500);
34705
+ } catch (err) {
34706
+ if (panel) panel.innerHTML = '<span style="font-size:11px;color:var(--red)">Failed: ' + esc(String(err)) + '</span>';
34707
+ }
34708
+ }
34709
+
34710
+ async function dismissBackgroundOffer(id, panel) {
34711
+ try {
34712
+ await apiFetch('/api/background-offers/' + encodeURIComponent(id) + '/dismiss', { method: 'POST' });
34713
+ } catch (_) { /* best-effort */ }
34714
+ if (panel) panel.remove();
34715
+ }
34716
+
34517
34717
  let chatHistory = [];
34518
34718
  async function sendChat() {
34519
34719
  const input = document.getElementById('chat-input');
@@ -34621,6 +34821,7 @@ async function sendChat() {
34621
34821
  finalText = evt.response || finalText || 'No response';
34622
34822
  finalTrace = evt.trace || null;
34623
34823
  renderAssistantText(finalText);
34824
+ appendBackgroundOfferActions(asstBubble, finalText);
34624
34825
  } else if (evt.type === 'error') {
34625
34826
  throw new Error(evt.error || 'Stream error');
34626
34827
  }
@@ -35220,7 +35421,7 @@ document.addEventListener('click', function(e) {
35220
35421
  // Back-compat shim — older call sites still reference loadProfiles().
35221
35422
  function loadProfiles() { return refreshChatAgentPicker(); }
35222
35423
 
35223
- // ── Skill Studio — opens the Skills page composer ──────────
35424
+ // ── Skill Studio — opens the natural-language composer as a real modal ──────────
35224
35425
 
35225
35426
  function openSkillStudio() {
35226
35427
  navigateTo('skills');
@@ -35228,15 +35429,56 @@ function openSkillStudio() {
35228
35429
  try { initSkillComposer(); } catch (_) { /* non-fatal */ }
35229
35430
  var composer = document.getElementById('skill-composer');
35230
35431
  var input = document.getElementById('skill-composer-text');
35231
- if (composer && composer.scrollIntoView) composer.scrollIntoView({ behavior: 'smooth', block: 'start' });
35232
- if (composer) {
35233
- composer.style.boxShadow = '0 0 0 2px rgba(255,141,0,0.35)';
35234
- setTimeout(function() { composer.style.boxShadow = ''; }, 1400);
35432
+ var modal = document.getElementById('skill-studio-modal');
35433
+ if (!modal) {
35434
+ modal = document.createElement('div');
35435
+ modal.id = 'skill-studio-modal';
35436
+ modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.42);z-index:1050;display:none;align-items:center;justify-content:center;padding:20px';
35437
+ modal.innerHTML =
35438
+ '<div style="width:min(1040px,96vw);max-height:92vh;background:var(--bg-primary);border:1px solid var(--border);border-radius:10px;box-shadow:0 18px 56px rgba(0,0,0,0.35);display:flex;flex-direction:column;overflow:hidden">'
35439
+ + '<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:14px;padding:16px 20px;border-bottom:1px solid var(--border);background:var(--bg-secondary)">'
35440
+ + '<div style="min-width:0">'
35441
+ + '<div style="font-size:16px;font-weight:600;color:var(--text-primary);margin-bottom:3px">Skill Studio</div>'
35442
+ + '<div style="font-size:12px;color:var(--text-muted);line-height:1.45">Start with the outcome. Add MCP, CLI, project, or memory anchors only when they are real dependencies. Review the generated SKILL.md before saving.</div>'
35443
+ + '</div>'
35444
+ + '<button type="button" onclick="closeSkillStudio()" title="Close Skill Studio" style="background:none;border:none;font-size:20px;color:var(--text-muted);cursor:pointer;padding:0 4px;line-height:1">&times;</button>'
35445
+ + '</div>'
35446
+ + '<div id="skill-studio-modal-body" style="padding:18px 20px;overflow:auto;flex:1;min-height:0"></div>'
35447
+ + '<div style="display:flex;align-items:center;gap:8px;justify-content:flex-end;padding:14px 20px;border-top:1px solid var(--border);background:var(--bg-secondary)">'
35448
+ + '<button type="button" onclick="closeSkillStudio()" style="font-size:13px;padding:7px 14px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text-primary);cursor:pointer">Keep on page</button>'
35449
+ + '<button type="button" onclick="startSkillComposerChat()" style="font-size:13px;padding:7px 14px;border-radius:6px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text-primary);cursor:pointer">Build in chat</button>'
35450
+ + '<button type="button" onclick="startSkillComposerDraft()" class="btn-primary" style="font-size:13px;padding:7px 16px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-weight:600;cursor:pointer">Review draft</button>'
35451
+ + '</div>'
35452
+ + '</div>';
35453
+ document.body.appendChild(modal);
35454
+ }
35455
+ var body = document.getElementById('skill-studio-modal-body');
35456
+ if (composer && body && composer.parentElement !== body) {
35457
+ composer.dataset.originalMargin = composer.style.margin || '';
35458
+ body.appendChild(composer);
35459
+ composer.style.margin = '0';
35235
35460
  }
35461
+ modal.style.display = 'flex';
35462
+ updateSkillComposerDraftState();
35236
35463
  if (input) input.focus();
35237
35464
  }, 80);
35238
35465
  }
35239
35466
 
35467
+ function closeSkillStudio(opts) {
35468
+ opts = opts || {};
35469
+ var modal = document.getElementById('skill-studio-modal');
35470
+ var composer = document.getElementById('skill-composer');
35471
+ var home = document.getElementById('skill-composer-home');
35472
+ if (composer && home && composer.parentElement !== home) {
35473
+ home.appendChild(composer);
35474
+ composer.style.margin = composer.dataset.originalMargin || '0 0 16px';
35475
+ }
35476
+ if (modal) modal.style.display = 'none';
35477
+ if (!opts.silent && composer && composer.scrollIntoView) {
35478
+ composer.scrollIntoView({ behavior: 'smooth', block: 'start' });
35479
+ }
35480
+ }
35481
+
35240
35482
  function updateBuilderMode() {
35241
35483
  var type = (document.getElementById('builder-type') || {}).value || 'skill';
35242
35484
  var title = document.getElementById('builder-page-title');
@@ -5,7 +5,7 @@
5
5
  * Manages per-user/channel sessions for conversation continuity.
6
6
  */
7
7
  import { PersonalAssistant, type ProjectMeta } from '../agent/assistant.js';
8
- import type { OnProgressCallback, OnTextCallback, OnToolActivityCallback, SelfImproveConfig, SelfImproveExperiment, SessionProvenance, TeamMessage, VerboseLevel, WorkflowDefinition } from '../types.js';
8
+ import type { BackgroundTask, OnProgressCallback, OnTextCallback, OnToolActivityCallback, SelfImproveConfig, SelfImproveExperiment, SessionProvenance, TeamMessage, VerboseLevel, WorkflowDefinition } from '../types.js';
9
9
  import { AgentManager } from '../agent/agent-manager.js';
10
10
  import { TeamRouter } from '../agent/team-router.js';
11
11
  import { TeamBus } from '../agent/team-bus.js';
@@ -23,6 +23,7 @@ export declare class Gateway {
23
23
  private approvalResolvers;
24
24
  private approvalCounter;
25
25
  private sessions;
26
+ private pendingBackgroundOffers;
26
27
  private auditLog;
27
28
  private draining;
28
29
  /** Side-channel for the metadata of the *most recent* cron run, populated
@@ -53,6 +54,25 @@ export declare class Gateway {
53
54
  private isTrustedPersonalSession;
54
55
  private runningUnleashedTasks;
55
56
  private extractBackgroundTaskId;
57
+ private makeBackgroundOfferId;
58
+ private backgroundAgentForSession;
59
+ private pruneExpiredBackgroundOffers;
60
+ private latestBackgroundOfferForSession;
61
+ private getBackgroundOfferForSession;
62
+ private createBackgroundOffer;
63
+ private queueBackgroundOffer;
64
+ private formatBackgroundQueuedResponse;
65
+ private formatBackgroundOfferResponse;
66
+ acceptBackgroundOffer(sessionKey: string, id: string): {
67
+ ok: boolean;
68
+ response: string;
69
+ task?: BackgroundTask;
70
+ };
71
+ dismissBackgroundOffer(sessionKey: string, id: string): {
72
+ ok: boolean;
73
+ response: string;
74
+ };
75
+ private resolveBackgroundOfferControl;
56
76
  private isAgentScopedSession;
57
77
  private readUnleashedStatus;
58
78
  private taskElapsedMinutes;
@@ -16,7 +16,7 @@ import { lanes } from './lanes.js';
16
16
  import { AgentManager } from '../agent/agent-manager.js';
17
17
  import { TeamRouter } from '../agent/team-router.js';
18
18
  import { TeamBus } from '../agent/team-bus.js';
19
- import { listBackgroundTasks, loadBackgroundTask, markFailed } from '../agent/background-tasks.js';
19
+ import { createBackgroundTask, listBackgroundTasks, loadBackgroundTask, markFailed } from '../agent/background-tasks.js';
20
20
  import { applyAssistantExperienceUpdate, detectApprovalReply, detectLocalTurn } from '../agent/local-turn.js';
21
21
  import { buildApprovalFollowupPrompt, detectActionExpectation } from '../agent/action-enforcer.js';
22
22
  import { updateClementineJson } from '../config/clementine-json.js';
@@ -36,6 +36,7 @@ import { isLiveUnleashedStatus } from './unleashed-status.js';
36
36
  import { buildActiveContextSnapshot } from './active-context.js';
37
37
  import { markContextEventBySource } from './context-events.js';
38
38
  import { EventLog } from './event-log.js';
39
+ import { detectComplexTaskForBackground } from '../agent/complex-task-detector.js';
39
40
  export { isLiveUnleashedStatus } from './unleashed-status.js';
40
41
  const logger = pino({ name: 'clementine.gateway' });
41
42
  const INTERACTIVE_FAILURE_LOG = path.join(BASE_DIR, 'self-improve', 'interactive-failures.jsonl');
@@ -130,6 +131,7 @@ export class Gateway {
130
131
  approvalResolvers = new Map();
131
132
  approvalCounter = 0;
132
133
  sessions = new Map();
134
+ pendingBackgroundOffers = new Map();
133
135
  auditLog = [];
134
136
  draining = false;
135
137
  /** Side-channel for the metadata of the *most recent* cron run, populated
@@ -245,15 +247,157 @@ export class Gateway {
245
247
  extractBackgroundTaskId(text) {
246
248
  return text.match(BACKGROUND_TASK_ID_RE)?.[0]?.toLowerCase();
247
249
  }
250
+ makeBackgroundOfferId() {
251
+ return `bo-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
252
+ }
253
+ backgroundAgentForSession(sessionKey) {
254
+ return this._agentSlugFromSessionKey(sessionKey) ?? this.getSessionProfile(sessionKey) ?? 'clementine';
255
+ }
256
+ pruneExpiredBackgroundOffers() {
257
+ const now = Date.now();
258
+ for (const [id, offer] of this.pendingBackgroundOffers) {
259
+ if (offer.expiresAt <= now)
260
+ this.pendingBackgroundOffers.delete(id);
261
+ }
262
+ }
263
+ latestBackgroundOfferForSession(sessionKey) {
264
+ this.pruneExpiredBackgroundOffers();
265
+ const offers = [...this.pendingBackgroundOffers.values()]
266
+ .filter((offer) => offer.sessionKey === sessionKey)
267
+ .sort((a, b) => b.createdAt - a.createdAt);
268
+ return offers[0];
269
+ }
270
+ getBackgroundOfferForSession(sessionKey, id) {
271
+ this.pruneExpiredBackgroundOffers();
272
+ const offer = this.pendingBackgroundOffers.get(id);
273
+ if (!offer || offer.sessionKey !== sessionKey)
274
+ return undefined;
275
+ return offer;
276
+ }
277
+ createBackgroundOffer(sessionKey, prompt, recommendation) {
278
+ this.pruneExpiredBackgroundOffers();
279
+ const offer = {
280
+ id: this.makeBackgroundOfferId(),
281
+ sessionKey,
282
+ fromAgent: this.backgroundAgentForSession(sessionKey),
283
+ prompt,
284
+ recommendation,
285
+ createdAt: Date.now(),
286
+ expiresAt: Date.now() + 30 * 60_000,
287
+ };
288
+ this.pendingBackgroundOffers.set(offer.id, offer);
289
+ return offer;
290
+ }
291
+ queueBackgroundOffer(offer) {
292
+ const task = createBackgroundTask({
293
+ fromAgent: offer.fromAgent,
294
+ prompt: offer.prompt,
295
+ maxMinutes: offer.recommendation.suggestedMaxMinutes,
296
+ sessionKey: offer.sessionKey,
297
+ });
298
+ this.pendingBackgroundOffers.delete(offer.id);
299
+ logger.info({
300
+ offerId: offer.id,
301
+ taskId: task.id,
302
+ sessionKey: offer.sessionKey,
303
+ fromAgent: offer.fromAgent,
304
+ maxMinutes: task.maxMinutes,
305
+ }, 'Queued background task from chat offer');
306
+ return task;
307
+ }
308
+ formatBackgroundQueuedResponse(task) {
309
+ return [
310
+ `Queued background task **${task.id}**.`,
311
+ '',
312
+ `It will run as **${task.fromAgent}** with a ${task.maxMinutes} minute cap.`,
313
+ `Use \`status ${task.id}\` or check the dashboard Background Tasks panel for progress.`,
314
+ ].join('\n');
315
+ }
316
+ formatBackgroundOfferResponse(offer) {
317
+ const lines = [
318
+ 'This looks like long-running, multi-tool work. I recommend running it in the background so chat does not go stale.',
319
+ '',
320
+ '**Plan**',
321
+ ...offer.recommendation.plan.map((step, idx) => `${idx + 1}. ${step}`),
322
+ '',
323
+ `**Why background:** ${offer.recommendation.reasons.join('; ')}.`,
324
+ `**Estimated cap:** ${offer.recommendation.suggestedMaxMinutes} minutes.`,
325
+ `**Background offer:** ${offer.id}`,
326
+ '',
327
+ `Reply \`run background ${offer.id}\` to queue it, \`run inline ${offer.id}\` to run it in this chat, or \`save skill ${offer.id}\` to make it reusable first.`,
328
+ ];
329
+ return lines.join('\n');
330
+ }
331
+ acceptBackgroundOffer(sessionKey, id) {
332
+ const offer = this.getBackgroundOfferForSession(sessionKey, id);
333
+ if (!offer) {
334
+ return { ok: false, response: `I could not find an active background offer for ${id}. It may have expired.` };
335
+ }
336
+ const task = this.queueBackgroundOffer(offer);
337
+ return { ok: true, response: this.formatBackgroundQueuedResponse(task), task };
338
+ }
339
+ dismissBackgroundOffer(sessionKey, id) {
340
+ const offer = this.getBackgroundOfferForSession(sessionKey, id);
341
+ if (!offer)
342
+ return { ok: false, response: `No active background offer found for ${id}.` };
343
+ this.pendingBackgroundOffers.delete(id);
344
+ return { ok: true, response: `Dismissed background offer ${id}.` };
345
+ }
346
+ resolveBackgroundOfferControl(sessionKey, text) {
347
+ const normalized = text.trim().toLowerCase();
348
+ const explicitOfferId = text.match(/\bbo-[a-z0-9]+-[a-z0-9]{3,10}\b/i)?.[0]?.toLowerCase();
349
+ const offer = explicitOfferId
350
+ ? this.getBackgroundOfferForSession(sessionKey, explicitOfferId)
351
+ : this.latestBackgroundOfferForSession(sessionKey);
352
+ if (!offer)
353
+ return null;
354
+ if (/^(run|start|queue|approve|yes|go|do it).{0,30}\bbackground\b/i.test(normalized)
355
+ || /^run it in the background\b/i.test(normalized)
356
+ || /^background\b/i.test(normalized)
357
+ || /^(yes|yep|approved?|go|do it|please do|start it)$/i.test(normalized)) {
358
+ const task = this.queueBackgroundOffer(offer);
359
+ return {
360
+ response: this.formatBackgroundQueuedResponse(task),
361
+ executionMode: 'background_queued',
362
+ backgroundTaskId: task.id,
363
+ };
364
+ }
365
+ if (/^run.{0,30}\binline\b/i.test(normalized) || /^run inline\b/i.test(normalized)) {
366
+ this.pendingBackgroundOffers.delete(offer.id);
367
+ return { inlineText: offer.prompt, executionMode: 'inline' };
368
+ }
369
+ if (/^(save|create|make|teach).{0,30}\bskill\b/i.test(normalized)) {
370
+ this.pendingBackgroundOffers.delete(offer.id);
371
+ return {
372
+ skillText: [
373
+ 'Create a reusable Clementine skill for this workflow using skill-creator principles.',
374
+ 'Write a concise Anthropic-compatible folder-form SKILL.md with clear trigger description, required tools/MCP/CLI dependencies, procedure, success criteria, and failure handling.',
375
+ '',
376
+ 'Original workflow request:',
377
+ offer.prompt,
378
+ ].join('\n'),
379
+ executionMode: 'inline',
380
+ };
381
+ }
382
+ return null;
383
+ }
248
384
  isAgentScopedSession(sessionKey) {
249
385
  return this._agentSlugFromSessionKey(sessionKey) !== undefined;
250
386
  }
251
387
  readUnleashedStatus(jobName) {
252
388
  try {
253
- const statusPath = path.join(BASE_DIR, 'unleashed', jobName, 'status.json');
254
- if (!existsSync(statusPath))
255
- return null;
256
- return JSON.parse(readFileSync(statusPath, 'utf-8'));
389
+ const candidates = [
390
+ jobName,
391
+ jobName.startsWith('bg-') ? `bg:${jobName}` : '',
392
+ ].filter(Boolean);
393
+ for (const candidate of candidates) {
394
+ const safeJob = candidate.replace(/[^a-zA-Z0-9_-]/g, '_');
395
+ const statusPath = path.join(BASE_DIR, 'unleashed', safeJob, 'status.json');
396
+ if (existsSync(statusPath)) {
397
+ return JSON.parse(readFileSync(statusPath, 'utf-8'));
398
+ }
399
+ }
400
+ return null;
257
401
  }
258
402
  catch {
259
403
  return null;
@@ -300,10 +444,14 @@ export class Gateway {
300
444
  : (task.status === 'failed' || task.status === 'aborted') && task.error
301
445
  ? ` Reason: ${this.taskSummary(task.error, 120)}`
302
446
  : '';
303
- return `- ${task.id}: ${task.status}${phase}, ${elapsed} min${cap}. ${taskText}${terminalDetail}`;
447
+ const deliverable = task.status === 'done' && task.deliverableNote
448
+ ? ` Deliverable: ${this.taskSummary(task.deliverableNote, 120)}`
449
+ : '';
450
+ return `- ${task.id}: ${task.status}${phase}, ${elapsed} min${cap}. ${taskText}${terminalDetail}${deliverable}`;
304
451
  }
305
452
  writeUnleashedCancel(jobName) {
306
- const cancelDir = path.join(BASE_DIR, 'unleashed', jobName);
453
+ const safeJob = (jobName.startsWith('bg-') ? `bg:${jobName}` : jobName).replace(/[^a-zA-Z0-9_-]/g, '_');
454
+ const cancelDir = path.join(BASE_DIR, 'unleashed', safeJob);
307
455
  mkdirSync(cancelDir, { recursive: true });
308
456
  writeFileSync(path.join(cancelDir, 'CANCEL'), '');
309
457
  }
@@ -1312,6 +1460,9 @@ export class Gateway {
1312
1460
  actionExpectationSource: actionExpectationForLedger.source,
1313
1461
  actionExpectationReason: actionExpectationForLedger.reason,
1314
1462
  runId: ledgerRunMetadata.runId,
1463
+ executionMode: ledgerRunMetadata.executionMode,
1464
+ backgroundTaskId: ledgerRunMetadata.backgroundTaskId,
1465
+ skillsApplied: ledgerRunMetadata.skillsApplied,
1315
1466
  permissionModeApplied: ledgerRunMetadata.permissionModeApplied,
1316
1467
  allowedToolsApplied: ledgerRunMetadata.allowedToolsApplied?.slice(0, 80),
1317
1468
  builtinToolsApplied: ledgerRunMetadata.builtinToolsApplied?.slice(0, 40),
@@ -1332,7 +1483,8 @@ export class Gateway {
1332
1483
  });
1333
1484
  }
1334
1485
  async _handleMessageInner(sessionKey, text, onText, model, maxTurns, onToolActivity, onProgress, ledgerRunMetadata) {
1335
- const originalText = text;
1486
+ let originalText = text;
1487
+ let skipBackgroundOffer = false;
1336
1488
  // Per-segment latency capture — emitted as a single 'chat:latency' line
1337
1489
  // on the happy path so we can grep/aggregate without parsing many lines.
1338
1490
  const tInnerStart = Date.now();
@@ -1438,6 +1590,30 @@ export class Gateway {
1438
1590
  }, 'chat:latency');
1439
1591
  return localResponse;
1440
1592
  }
1593
+ const backgroundControl = this.resolveBackgroundOfferControl(sessionKey, text);
1594
+ if (backgroundControl?.response) {
1595
+ if (ledgerRunMetadata) {
1596
+ ledgerRunMetadata.executionMode = backgroundControl.executionMode;
1597
+ ledgerRunMetadata.backgroundTaskId = backgroundControl.backgroundTaskId;
1598
+ }
1599
+ if (onText) {
1600
+ try {
1601
+ await onText(backgroundControl.response);
1602
+ }
1603
+ catch { /* channel streaming is best-effort */ }
1604
+ }
1605
+ return backgroundControl.response;
1606
+ }
1607
+ if (backgroundControl?.inlineText) {
1608
+ text = backgroundControl.inlineText;
1609
+ originalText = backgroundControl.inlineText;
1610
+ skipBackgroundOffer = true;
1611
+ }
1612
+ else if (backgroundControl?.skillText) {
1613
+ text = backgroundControl.skillText;
1614
+ originalText = backgroundControl.skillText;
1615
+ skipBackgroundOffer = true;
1616
+ }
1441
1617
  const approvalFollowupExpected = this.isTrustedPersonalSession(sessionKey)
1442
1618
  && detectApprovalReply(originalText) === true
1443
1619
  && this.assistant.hasRecentApprovalPrompt(sessionKey);
@@ -1685,6 +1861,46 @@ export class Gateway {
1685
1861
  const resolvedProfile = profileSlug
1686
1862
  ? this.getAgentManager().get(profileSlug) ?? undefined
1687
1863
  : undefined;
1864
+ const isBuilderSession = sessionKey.startsWith('dashboard:builder:');
1865
+ if (!skipBackgroundOffer && !isBuilderSession && !isInternalMsg && this.isTrustedPersonalSession(sessionKey)) {
1866
+ const recommendation = detectComplexTaskForBackground(text);
1867
+ if (recommendation) {
1868
+ const offer = this.createBackgroundOffer(sessionKey, text, recommendation);
1869
+ if (recommendation.queueImmediately) {
1870
+ const task = this.queueBackgroundOffer(offer);
1871
+ const queued = this.formatBackgroundQueuedResponse(task);
1872
+ if (ledgerRunMetadata) {
1873
+ ledgerRunMetadata.executionMode = 'background_queued';
1874
+ ledgerRunMetadata.backgroundTaskId = task.id;
1875
+ }
1876
+ if (onText) {
1877
+ try {
1878
+ await onText(queued);
1879
+ }
1880
+ catch { /* channel streaming is best-effort */ }
1881
+ }
1882
+ return queued;
1883
+ }
1884
+ const offerText = this.formatBackgroundOfferResponse(offer);
1885
+ if (ledgerRunMetadata) {
1886
+ ledgerRunMetadata.executionMode = 'background_offer';
1887
+ }
1888
+ logger.info({
1889
+ sessionKey,
1890
+ offerId: offer.id,
1891
+ score: recommendation.score,
1892
+ reasons: recommendation.reasons,
1893
+ maxMinutes: recommendation.suggestedMaxMinutes,
1894
+ }, 'Offering background execution for complex chat request');
1895
+ if (onText) {
1896
+ try {
1897
+ await onText(offerText);
1898
+ }
1899
+ catch { /* channel streaming is best-effort */ }
1900
+ }
1901
+ return offerText;
1902
+ }
1903
+ }
1688
1904
  const hygiene = assessGatewayContextHygiene({
1689
1905
  sessionKey: effectiveSessionKey,
1690
1906
  textChars: enrichedText.length,
@@ -1804,7 +2020,6 @@ export class Gateway {
1804
2020
  // the agent only emits json-artifact blocks. Strip everything
1805
2021
  // expensive; keep just SDK session resume so multi-turn
1806
2022
  // artifact iteration sees its own prior turns.
1807
- const isBuilderSession = sessionKey.startsWith('dashboard:builder:');
1808
2023
  // ── Skill auto-match (1.18.170) ─────────────────────────────
1809
2024
  // Match the user's message against the skill catalog (auto-
1810
2025
  // skills + user-authored). Top-3 matches above score ≥ 4 inform:
@@ -1903,7 +2118,6 @@ export class Gateway {
1903
2118
  ...(chatMcp ? { extraMcpServers: chatMcp.servers } : {}),
1904
2119
  onText: wrappedOnText,
1905
2120
  onToolActivity: ({ tool, input }) => {
1906
- toolActivityCount++;
1907
2121
  if (wrappedOnToolActivity) {
1908
2122
  return wrappedOnToolActivity(tool, input);
1909
2123
  }
@@ -1913,6 +2127,12 @@ export class Gateway {
1913
2127
  });
1914
2128
  if (ledgerRunMetadata) {
1915
2129
  ledgerRunMetadata.runId = runAgentResult.runId;
2130
+ ledgerRunMetadata.executionMode = ledgerRunMetadata.executionMode ?? 'inline';
2131
+ ledgerRunMetadata.skillsApplied = resolvedSkills?.matches.map((m) => ({
2132
+ name: m.name,
2133
+ source: 'auto',
2134
+ score: m.score,
2135
+ }));
1916
2136
  ledgerRunMetadata.permissionModeApplied = runAgentResult.permissionMode;
1917
2137
  ledgerRunMetadata.allowedToolsApplied = runAgentResult.allowedToolsApplied;
1918
2138
  ledgerRunMetadata.builtinToolsApplied = runAgentResult.builtinToolsApplied;
@@ -17,6 +17,13 @@ export interface TurnLedgerEntry {
17
17
  actionExpectationSource?: string;
18
18
  actionExpectationReason?: string;
19
19
  runId?: string;
20
+ executionMode?: 'inline' | 'background_offer' | 'background_queued';
21
+ backgroundTaskId?: string;
22
+ skillsApplied?: Array<{
23
+ name: string;
24
+ source: 'auto' | 'pinned';
25
+ score?: number;
26
+ }>;
20
27
  permissionModeApplied?: string;
21
28
  allowedToolsApplied?: string[];
22
29
  builtinToolsApplied?: string[];
@@ -40,7 +40,12 @@ export function formatLastTurnLedger(sessionKey, baseDir = BASE_DIR) {
40
40
  ? `Tools used: ${last.toolCallsMade} (${last.toolNames.slice(0, 6).join(', ')}${last.toolNames.length > 6 ? ', ...' : ''}).`
41
41
  : 'Tools used: none.';
42
42
  const execution = last.permissionModeApplied || last.mcpServersApplied?.length
43
- ? `Execution: ${last.permissionModeApplied ?? 'unknown'}${last.mcpServersApplied?.length ? `; MCP: ${last.mcpServersApplied.slice(0, 6).join(', ')}${last.mcpServersApplied.length > 6 ? ', ...' : ''}` : ''}.`
43
+ ? `Execution: ${last.executionMode ?? 'inline'}; ${last.permissionModeApplied ?? 'unknown'}${last.mcpServersApplied?.length ? `; MCP: ${last.mcpServersApplied.slice(0, 6).join(', ')}${last.mcpServersApplied.length > 6 ? ', ...' : ''}` : ''}.`
44
+ : last.executionMode
45
+ ? `Execution: ${last.executionMode}${last.backgroundTaskId ? ` (${last.backgroundTaskId})` : ''}.`
46
+ : '';
47
+ const skills = last.skillsApplied?.length
48
+ ? `Skills: ${last.skillsApplied.slice(0, 5).map((s) => s.name).join(', ')}${last.skillsApplied.length > 5 ? ', ...' : ''}.`
44
49
  : '';
45
50
  const response = last.responsePreview
46
51
  ? `Last response: "${last.responsePreview.replace(/\s+/g, ' ').slice(0, 240)}${last.responsePreview.length > 240 ? '...' : ''}"`
@@ -54,6 +59,7 @@ export function formatLastTurnLedger(sessionKey, baseDir = BASE_DIR) {
54
59
  `Toolset: ${last.toolset ?? 'auto'}.`,
55
60
  `Policy: ${last.policyReason ?? 'unknown'}; tools ${last.toolsEnabled ? 'enabled' : 'disabled'}.`,
56
61
  execution,
62
+ skills,
57
63
  response,
58
64
  ].filter(Boolean).join('\n');
59
65
  }
@@ -65,6 +65,10 @@ export function registerBackgroundTaskTools(server) {
65
65
  lines.push('');
66
66
  lines.push(`Result:\n${task.result}`);
67
67
  }
68
+ if (task.resultPath) {
69
+ lines.push('');
70
+ lines.push(`Full result: ${task.resultPath}`);
71
+ }
68
72
  if (task.deliverableNote) {
69
73
  lines.push('');
70
74
  lines.push(`Deliverable: ${task.deliverableNote}`);
package/dist/types.d.ts CHANGED
@@ -284,6 +284,7 @@ export interface BackgroundTask {
284
284
  startedAt?: string;
285
285
  completedAt?: string;
286
286
  result?: string;
287
+ resultPath?: string;
287
288
  error?: string;
288
289
  deliverableNote?: string;
289
290
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.170",
3
+ "version": "1.18.172",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",