clementine-agent 1.18.170 → 1.18.171

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
  }
@@ -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();
@@ -21628,9 +21662,13 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
21628
21662
  <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
21663
  </div>
21630
21664
  <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">
21665
+ <div style="display:flex;gap:6px;align-items:center">
21666
+ <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">
21667
+ <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>
21668
+ </div>
21632
21669
  <datalist id="skill-composer-anchor-options"></datalist>
21633
21670
  <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>
21671
+ <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
21672
  <div style="display:flex;align-items:center;justify-content:flex-end;gap:8px;margin-top:12px">
21635
21673
  <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
21674
  <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>
@@ -28725,6 +28763,7 @@ var _skillsState = {
28725
28763
 
28726
28764
  var _skillComposerMode = 'outcome';
28727
28765
  var _skillComposerOptionsCache = null;
28766
+ var _skillComposerDependencies = [];
28728
28767
 
28729
28768
  var _skillComposerCopy = {
28730
28769
  outcome: {
@@ -28785,34 +28824,77 @@ function updateSkillComposerDraftState() {
28785
28824
  var text = (document.getElementById('skill-composer-text') || {}).value || '';
28786
28825
  var anchor = (document.getElementById('skill-composer-anchor') || {}).value || '';
28787
28826
  var btn = document.getElementById('skill-composer-draft-btn');
28788
- var enabled = !!(text.trim() || anchor.trim());
28827
+ var enabled = !!(text.trim() || anchor.trim() || _skillComposerDependencies.length);
28789
28828
  if (btn) {
28790
28829
  btn.disabled = !enabled;
28791
28830
  btn.style.opacity = enabled ? '1' : '0.55';
28792
28831
  btn.style.cursor = enabled ? 'pointer' : 'not-allowed';
28793
28832
  }
28794
28833
  renderSkillComposerAnchorSummary(text, anchor);
28834
+ renderSkillComposerPreview();
28795
28835
  }
28796
28836
 
28797
28837
  function renderSkillComposerAnchorSummary(text, anchor) {
28798
28838
  var box = document.getElementById('skill-composer-anchor-summary');
28799
28839
  if (!box) return;
28800
28840
  var trimmed = String(anchor || '').trim();
28801
- if (!trimmed) {
28841
+ var deps = getSkillComposerDependencies(false);
28842
+ if (!trimmed && deps.length === 0) {
28802
28843
  box.innerHTML = '<span style="color:var(--text-muted)">No starting point selected.</span>';
28803
28844
  return;
28804
28845
  }
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>';
28846
+ var html = '<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">';
28847
+ for (var i = 0; i < deps.length; i++) {
28848
+ var dep = deps[i];
28849
+ var copy = _skillComposerCopy[dep.mode] || _skillComposerCopy.outcome;
28850
+ 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">'
28851
+ + '<strong style="color:var(--accent);font-weight:600">' + esc(copy.promptLabel) + '</strong>'
28852
+ + '<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:190px">' + esc(dep.value) + '</span>'
28853
+ + '<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>'
28854
+ + '</span>';
28855
+ }
28856
+ if (trimmed) {
28857
+ var modeCopy = _skillComposerCopy[_skillComposerMode] || _skillComposerCopy.outcome;
28858
+ var tools = inferSkillComposerTools(_skillComposerMode, trimmed);
28859
+ html += '<span style="color:var(--text-muted)">Pending ' + esc(modeCopy.promptLabel) + ':</span>'
28860
+ + '<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>'
28861
+ + (tools.length ? '<span style="color:var(--text-muted)">adds ' + esc(tools.join(', ')) + '</span>' : '');
28862
+ }
28863
+ html += '</div>';
28864
+ box.innerHTML = html;
28865
+ }
28866
+
28867
+ function getSkillComposerDependencies(includePending) {
28868
+ var deps = _skillComposerDependencies.slice();
28869
+ if (includePending) {
28870
+ var pending = ((document.getElementById('skill-composer-anchor') || {}).value || '').trim();
28871
+ if (pending) deps.push({ mode: _skillComposerMode || 'outcome', value: pending });
28872
+ }
28873
+ var seen = {};
28874
+ return deps.filter(function(dep) {
28875
+ var key = dep.mode + ':' + dep.value;
28876
+ if (seen[key]) return false;
28877
+ seen[key] = true;
28878
+ return true;
28879
+ });
28880
+ }
28881
+
28882
+ function addSkillComposerDependency() {
28883
+ var input = document.getElementById('skill-composer-anchor');
28884
+ var value = (input && input.value || '').trim();
28885
+ if (!value) return;
28886
+ var mode = _skillComposerMode || 'outcome';
28887
+ var key = mode + ':' + value;
28888
+ var exists = _skillComposerDependencies.some(function(dep) { return dep.mode + ':' + dep.value === key; });
28889
+ if (!exists) _skillComposerDependencies.push({ mode: mode, value: value });
28890
+ if (input) input.value = '';
28891
+ updateSkillComposerDraftState();
28892
+ }
28893
+
28894
+ function removeSkillComposerDependency(index) {
28895
+ if (index < 0 || index >= _skillComposerDependencies.length) return;
28896
+ _skillComposerDependencies.splice(index, 1);
28897
+ updateSkillComposerDraftState();
28816
28898
  }
28817
28899
 
28818
28900
  function inferSkillComposerTools(mode, anchor) {
@@ -28888,12 +28970,19 @@ function slugifySkillTitle(title) {
28888
28970
 
28889
28971
  function buildSkillComposerDraftSeed() {
28890
28972
  var text = ((document.getElementById('skill-composer-text') || {}).value || '').trim();
28891
- var anchor = ((document.getElementById('skill-composer-anchor') || {}).value || '').trim();
28892
28973
  var mode = _skillComposerMode || 'outcome';
28893
28974
  var modeCopy = _skillComposerCopy[mode] || _skillComposerCopy.outcome;
28975
+ var deps = getSkillComposerDependencies(true);
28976
+ var anchor = deps.map(function(dep) {
28977
+ var copy = _skillComposerCopy[dep.mode] || _skillComposerCopy.outcome;
28978
+ return copy.promptLabel + ': ' + dep.value;
28979
+ }).join('; ');
28894
28980
  var title = inferSkillComposerTitle(text, anchor);
28895
28981
  var name = slugifySkillTitle(title);
28896
- var tools = inferSkillComposerTools(mode, anchor);
28982
+ var tools = [];
28983
+ for (var di = 0; di < deps.length; di++) {
28984
+ tools = tools.concat(inferSkillComposerTools(deps[di].mode, deps[di].value));
28985
+ }
28897
28986
  var hay = (text + ' ' + anchor).toLowerCase();
28898
28987
  if (hay.indexOf('salesforce') !== -1) tools = dedupeSkillComposerToolNames(tools.concat(['Bash']));
28899
28988
  if (hay.indexOf('dataforseo') !== -1 && tools.indexOf('mcp__dataforseo__*') === -1) tools.push('mcp__dataforseo__*');
@@ -28909,13 +28998,18 @@ function buildSkillComposerDraftSeed() {
28909
28998
  if (description.length > 1024) description = description.slice(0, 1024);
28910
28999
 
28911
29000
  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';
29001
+ var anchorLine = deps.length
29002
+ ? deps.map(function(dep) {
29003
+ var copy = _skillComposerCopy[dep.mode] || _skillComposerCopy.outcome;
29004
+ return '- ' + copy.promptLabel + ': ' + dep.value;
29005
+ }).join('\\n')
29006
+ : '- None selected';
28913
29007
  var body = '# ' + title + '\\n\\n'
28914
29008
  + '## Use When\\n\\n'
28915
29009
  + description + '\\n\\n'
28916
29010
  + '## Starting Point\\n\\n'
28917
29011
  + '- Outcome: ' + (text || 'Fill in the exact user goal before saving.') + '\\n'
28918
- + '- ' + anchorLine + '\\n\\n'
29012
+ + anchorLine + '\\n\\n'
28919
29013
  + '## Required Tools\\n\\n'
28920
29014
  + toolLines + '\\n\\n'
28921
29015
  + '## Procedure\\n\\n'
@@ -28961,10 +29055,37 @@ function buildSkillComposerDraftSeed() {
28961
29055
  description: description,
28962
29056
  body: body,
28963
29057
  tools: tools,
28964
- note: anchor ? ('Seeded from ' + modeCopy.promptLabel + ': ' + anchor) : 'Seeded from the natural language description.',
29058
+ note: deps.length ? ('Seeded from ' + deps.length + ' starting point' + (deps.length === 1 ? '' : 's') + '.') : 'Seeded from the natural language description.',
28965
29059
  };
28966
29060
  }
28967
29061
 
29062
+ function renderSkillComposerPreview() {
29063
+ var box = document.getElementById('skill-composer-preview');
29064
+ if (!box) return;
29065
+ var text = ((document.getElementById('skill-composer-text') || {}).value || '').trim();
29066
+ var deps = getSkillComposerDependencies(true);
29067
+ if (!text && deps.length === 0) {
29068
+ box.innerHTML = '<span style="color:var(--text-muted)">Preview appears here as you describe the skill or add tools.</span>';
29069
+ return;
29070
+ }
29071
+ var seed = buildSkillComposerDraftSeed();
29072
+ var depHtml = deps.length
29073
+ ? deps.map(function(dep) {
29074
+ var copy = _skillComposerCopy[dep.mode] || _skillComposerCopy.outcome;
29075
+ 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>';
29076
+ }).join('')
29077
+ : '<span style="color:var(--text-muted)">No explicit dependencies yet.</span>';
29078
+ var toolsHtml = seed.tools.length
29079
+ ? 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('')
29080
+ : '<span style="color:var(--text-muted)">inherits default runtime surface</span>';
29081
+ box.innerHTML =
29082
+ '<div style="font-weight:600;color:var(--text-primary);margin-bottom:4px">' + esc(seed.title) + '</div>' +
29083
+ '<div style="color:var(--text-muted);margin-bottom:8px">' + esc(seed.name) + '</div>' +
29084
+ '<div style="margin-bottom:8px"><strong style="color:var(--text-secondary)">Dependencies</strong><br>' + depHtml + '</div>' +
29085
+ '<div style="margin-bottom:8px"><strong style="color:var(--text-secondary)">Tools</strong><br>' + toolsHtml + '</div>' +
29086
+ '<div><strong style="color:var(--text-secondary)">Trigger description</strong><br>' + esc(seed.description.slice(0, 260)) + (seed.description.length > 260 ? '...' : '') + '</div>';
29087
+ }
29088
+
28968
29089
  async function hydrateSkillComposerOptions() {
28969
29090
  if (_skillComposerOptionsCache) return _skillComposerOptionsCache;
28970
29091
  var cache = { tool: [], cli: [], project: [], memory: [] };
@@ -29062,15 +29183,21 @@ async function renderSkillComposerOptions() {
29062
29183
 
29063
29184
  function buildSkillComposerPrompt() {
29064
29185
  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;
29186
+ var deps = getSkillComposerDependencies(true);
29067
29187
  var lines = [
29068
29188
  'Use skill-creator principles to draft a Clementine skill in Skill Studio.',
29069
29189
  '',
29070
29190
  'Outcome: ' + (text || '(ask me for the outcome before drafting)'),
29071
- 'Starting point: ' + modeCopy.promptLabel,
29191
+ 'Starting points:',
29072
29192
  ];
29073
- if (anchor) lines.push('Anchor: ' + anchor);
29193
+ if (deps.length) {
29194
+ for (var i = 0; i < deps.length; i++) {
29195
+ var copy = _skillComposerCopy[deps[i].mode] || _skillComposerCopy.outcome;
29196
+ lines.push('- ' + copy.promptLabel + ': ' + deps[i].value);
29197
+ }
29198
+ } else {
29199
+ lines.push('- None selected');
29200
+ }
29074
29201
  lines.push(
29075
29202
  '',
29076
29203
  'Draft rules:',
@@ -34514,6 +34641,54 @@ function renderMd(text) {
34514
34641
  return s;
34515
34642
  }
34516
34643
 
34644
+ function extractBackgroundOfferId(text) {
34645
+ var m = String(text || '').match(/Background offer:\s*(bo-[a-z0-9]+-[a-z0-9]{3,10})/i);
34646
+ return m ? m[1] : '';
34647
+ }
34648
+
34649
+ function appendBackgroundOfferActions(bubble, text) {
34650
+ if (!bubble || bubble.querySelector('.chat-bg-offer-actions')) return;
34651
+ var id = extractBackgroundOfferId(text);
34652
+ if (!id) return;
34653
+ var panel = document.createElement('div');
34654
+ panel.className = 'chat-bg-offer-actions';
34655
+ panel.style.cssText = 'margin-top:10px;display:flex;gap:6px;flex-wrap:wrap;padding-top:8px;border-top:1px solid var(--border)';
34656
+ panel.innerHTML =
34657
+ '<button class="btn-sm btn-primary" data-bg-action="accept" style="font-size:11px;padding:5px 10px">Run in background</button>' +
34658
+ '<button class="btn-sm" data-bg-action="inline" style="font-size:11px;padding:5px 10px">Run inline anyway</button>' +
34659
+ '<button class="btn-sm" data-bg-action="skill" style="font-size:11px;padding:5px 10px">Save as skill first</button>' +
34660
+ '<button class="btn-sm" data-bg-action="dismiss" style="font-size:11px;padding:5px 10px;color:var(--text-muted)">Dismiss</button>';
34661
+ panel.querySelector('[data-bg-action="accept"]').onclick = function() { acceptBackgroundOffer(id, panel); };
34662
+ panel.querySelector('[data-bg-action="inline"]').onclick = function() { askClementineWith('run inline ' + id); };
34663
+ panel.querySelector('[data-bg-action="skill"]').onclick = function() { askClementineWith('save skill ' + id); };
34664
+ panel.querySelector('[data-bg-action="dismiss"]').onclick = function() { dismissBackgroundOffer(id, panel); };
34665
+ bubble.appendChild(panel);
34666
+ }
34667
+
34668
+ async function acceptBackgroundOffer(id, panel) {
34669
+ if (panel) panel.innerHTML = '<span style="font-size:11px;color:var(--text-muted)">Queueing background task...</span>';
34670
+ try {
34671
+ var r = await apiFetch('/api/background-offers/' + encodeURIComponent(id) + '/accept', { method: 'POST' });
34672
+ var d = await r.json();
34673
+ if (!r.ok || !d.ok) {
34674
+ if (panel) panel.innerHTML = '<span style="font-size:11px;color:var(--red)">' + esc(d.error || 'Failed to queue task') + '</span>';
34675
+ return;
34676
+ }
34677
+ if (panel) panel.innerHTML = '<div style="font-size:12px;line-height:1.45;color:var(--text-secondary)">' + renderMd(d.response || 'Background task queued.') + '</div>';
34678
+ toast('Background task queued', 'success');
34679
+ setTimeout(refreshAll, 500);
34680
+ } catch (err) {
34681
+ if (panel) panel.innerHTML = '<span style="font-size:11px;color:var(--red)">Failed: ' + esc(String(err)) + '</span>';
34682
+ }
34683
+ }
34684
+
34685
+ async function dismissBackgroundOffer(id, panel) {
34686
+ try {
34687
+ await apiFetch('/api/background-offers/' + encodeURIComponent(id) + '/dismiss', { method: 'POST' });
34688
+ } catch (_) { /* best-effort */ }
34689
+ if (panel) panel.remove();
34690
+ }
34691
+
34517
34692
  let chatHistory = [];
34518
34693
  async function sendChat() {
34519
34694
  const input = document.getElementById('chat-input');
@@ -34621,6 +34796,7 @@ async function sendChat() {
34621
34796
  finalText = evt.response || finalText || 'No response';
34622
34797
  finalTrace = evt.trace || null;
34623
34798
  renderAssistantText(finalText);
34799
+ appendBackgroundOfferActions(asstBubble, finalText);
34624
34800
  } else if (evt.type === 'error') {
34625
34801
  throw new Error(evt.error || 'Stream error');
34626
34802
  }
@@ -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.171",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",