clementine-agent 1.18.82 → 1.18.84

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.
@@ -70,8 +70,6 @@ export declare class PersonalAssistant {
70
70
  private memoryStore;
71
71
  private _lastUserMessage?;
72
72
  onSkillProposed: ((skill: import('../types.js').SkillDocument) => void) | null;
73
- private _lastMcpStatus;
74
- private _lastMcpStatusTime;
75
73
  /** Terminal reason from the last SDK query — consumed by cron scheduler for precise error classification. */
76
74
  private _lastTerminalReason?;
77
75
  /** Per-session stall nudge — set after a query shows stall signals, consumed on the next query. */
@@ -17,6 +17,7 @@ import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE,
17
17
  import { summarizeIntegrationStatus } from '../config/integrations-registry.js';
18
18
  import { loadToolPreferences, computeAvailability, buildPromptInstruction, buildComposioStatusBlock, KNOWN_SERVICES, } from '../integrations/tool-preferences.js';
19
19
  import { loadClaudeIntegrations } from './mcp-bridge.js';
20
+ import { getLatestMcpStatusSnapshot, recordMcpStatusFromSystemInit, invalidateMcpStatusEntry } from './run-agent.js';
20
21
  import { detectFrustrationSignals, detectRepeatedTopics } from './insight-engine.js';
21
22
  import { DEFAULT_CHANNEL_CAPABILITIES } from '../types.js';
22
23
  import { enforceToolPermissions, getSecurityPrompt, getHeartbeatSecurityPrompt, getCronSecurityPrompt, getHeartbeatDisallowedTools, logToolUse, logAuditJsonl, } from './hooks.js';
@@ -710,8 +711,10 @@ export class PersonalAssistant {
710
711
  memoryStore = null; // Typed as any — MemoryStore may not be available yet
711
712
  _lastUserMessage;
712
713
  onSkillProposed = null;
713
- _lastMcpStatus = [];
714
- _lastMcpStatusTime = '';
714
+ // PRD Phase 2 / 1.18.84: superseded by the shared module-level cache in
715
+ // run-agent.ts (getLatestMcpStatusSnapshot / recordMcpStatusFromSystemInit).
716
+ // The fields below were declared but never written pre-1.18.84; the
717
+ // module cache populates from every system/init message instead.
715
718
  /** Terminal reason from the last SDK query — consumed by cron scheduler for precise error classification. */
716
719
  _lastTerminalReason;
717
720
  /** Per-session stall nudge — set after a query shows stall signals, consumed on the next query. */
@@ -808,7 +811,12 @@ export class PersonalAssistant {
808
811
  this.onSkillProposed = cb;
809
812
  }
810
813
  getMcpStatus() {
811
- return { servers: this._lastMcpStatus, updatedAt: this._lastMcpStatusTime };
814
+ // 1.18.84 correctness fix: delegate to the shared module-level cache.
815
+ // Pre-1.18.84 we returned this._lastMcpStatus, which was declared but
816
+ // never written — getMcpStatus() always returned empty. Now run-agent
817
+ // and assistant query streams record into a shared snapshot via
818
+ // recordMcpStatusFromSystemInit when the SDK init message lands.
819
+ return getLatestMcpStatusSnapshot();
812
820
  }
813
821
  /**
814
822
  * PRD Phase 2.1: clear the cached status for one server so the next query
@@ -818,12 +826,9 @@ export class PersonalAssistant {
818
826
  * stale error/auth state. Returns the post-clear cached snapshot.
819
827
  */
820
828
  invalidateMcpStatus(serverName) {
821
- const beforeLen = this._lastMcpStatus.length;
822
- this._lastMcpStatus = this._lastMcpStatus.filter((s) => s.name !== serverName);
823
- const cleared = this._lastMcpStatus.length < beforeLen;
824
- if (cleared)
825
- this._lastMcpStatusTime = new Date().toISOString();
826
- return { servers: this._lastMcpStatus, updatedAt: this._lastMcpStatusTime, cleared };
829
+ const result = invalidateMcpStatusEntry(serverName);
830
+ const snapshot = getLatestMcpStatusSnapshot();
831
+ return { servers: snapshot.servers, updatedAt: snapshot.updatedAt, cleared: result.cleared };
827
832
  }
828
833
  /** Inject a background work result into the session as silent follow-up context. */
829
834
  injectPendingContext(sessionKey, userPrompt, result) {
@@ -2999,6 +3004,15 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
2999
3004
  const trace = [];
3000
3005
  const stream = query({ prompt, options: sdkOptions });
3001
3006
  for await (const message of stream) {
3007
+ // 1.18.84 correctness: capture MCP server status from SDK init.
3008
+ if (message.type === 'system' && message.subtype === 'init') {
3009
+ const mcpServersRaw = message.mcp_servers;
3010
+ if (mcpServersRaw)
3011
+ try {
3012
+ recordMcpStatusFromSystemInit(mcpServersRaw);
3013
+ }
3014
+ catch { /* non-fatal */ }
3015
+ }
3002
3016
  if (message.type === 'assistant') {
3003
3017
  const blocks = getContentBlocks(message);
3004
3018
  for (const block of blocks) {
@@ -22,6 +22,21 @@
22
22
  * long-task preflight, NO mode=unleashed wrapper.
23
23
  */
24
24
  import { type AgentDefinition } from '@anthropic-ai/claude-agent-sdk';
25
+ /** Read the latest MCP status snapshot. Safe to call from any module. */
26
+ export declare function getLatestMcpStatusSnapshot(): {
27
+ servers: Array<{
28
+ name: string;
29
+ status: string;
30
+ }>;
31
+ updatedAt: string;
32
+ };
33
+ /** Write a fresh snapshot. Called from system/init handlers. */
34
+ export declare function recordMcpStatusFromSystemInit(rawMcpServers: unknown): void;
35
+ /** Drop one server from the cache so the next query repopulates it. */
36
+ export declare function invalidateMcpStatusEntry(name: string): {
37
+ cleared: boolean;
38
+ updatedAt: string;
39
+ };
25
40
  import type { AgentProfile } from '../types.js';
26
41
  import type { AgentManager } from './agent-manager.js';
27
42
  import type { MemoryStore } from '../memory/store.js';
@@ -24,6 +24,49 @@
24
24
  import path from 'node:path';
25
25
  import { query } from '@anthropic-ai/claude-agent-sdk';
26
26
  import pino from 'pino';
27
+ /**
28
+ * Module-level cache of MCP server statuses from the most recent SDK
29
+ * init message. Populated by every runAgent / PersonalAssistant query
30
+ * stream that captures `system/init`. Read by Assistant.getMcpStatus()
31
+ * and the dashboard's Tools & MCP catalog page.
32
+ *
33
+ * Pre-1.18.84 the assistant declared a private _lastMcpStatus but no
34
+ * code wrote to it — getMcpStatus() always returned empty, making the
35
+ * catalog status pills misleading. The shared module cache fixes that
36
+ * without coupling assistant.ts to runAgent's stream loop.
37
+ */
38
+ let _lastMcpStatusSnapshot = {
39
+ servers: [],
40
+ updatedAt: '',
41
+ };
42
+ /** Read the latest MCP status snapshot. Safe to call from any module. */
43
+ export function getLatestMcpStatusSnapshot() {
44
+ return { servers: [..._lastMcpStatusSnapshot.servers], updatedAt: _lastMcpStatusSnapshot.updatedAt };
45
+ }
46
+ /** Write a fresh snapshot. Called from system/init handlers. */
47
+ export function recordMcpStatusFromSystemInit(rawMcpServers) {
48
+ if (!Array.isArray(rawMcpServers))
49
+ return;
50
+ const servers = [];
51
+ for (const entry of rawMcpServers) {
52
+ if (!entry || typeof entry !== 'object')
53
+ continue;
54
+ const e = entry;
55
+ if (typeof e.name !== 'string' || !e.name)
56
+ continue;
57
+ servers.push({ name: e.name, status: typeof e.status === 'string' ? e.status : 'unknown' });
58
+ }
59
+ _lastMcpStatusSnapshot = { servers, updatedAt: new Date().toISOString() };
60
+ }
61
+ /** Drop one server from the cache so the next query repopulates it. */
62
+ export function invalidateMcpStatusEntry(name) {
63
+ const before = _lastMcpStatusSnapshot.servers.length;
64
+ _lastMcpStatusSnapshot = {
65
+ servers: _lastMcpStatusSnapshot.servers.filter((s) => s.name !== name),
66
+ updatedAt: new Date().toISOString(),
67
+ };
68
+ return { cleared: _lastMcpStatusSnapshot.servers.length < before, updatedAt: _lastMcpStatusSnapshot.updatedAt };
69
+ }
27
70
  import { BASE_DIR, PKG_DIR, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
28
71
  import { buildAgentMap } from './agent-definitions.js';
29
72
  const MCP_SERVER_SCRIPT = path.join(PKG_DIR, 'dist', 'tools', 'mcp-server.js');
@@ -229,6 +272,17 @@ export async function runAgent(prompt, opts) {
229
272
  for await (const message of stream) {
230
273
  if (message.type === 'system' && message.subtype === 'init') {
231
274
  sessionId = message.session_id ?? '';
275
+ // PRD Phase 2 / 1.18.84 correctness: capture the SDK-reported MCP
276
+ // server status so getMcpStatus() (and the dashboard's Tools & MCP
277
+ // catalog) actually has data. The init message includes
278
+ // mcp_servers: Array<{ name, status }> per the SDK protocol.
279
+ const mcpServersRaw = message.mcp_servers;
280
+ if (mcpServersRaw) {
281
+ try {
282
+ recordMcpStatusFromSystemInit(mcpServersRaw);
283
+ }
284
+ catch { /* non-fatal */ }
285
+ }
232
286
  logger.debug({ sessionKey: opts.sessionKey, sdkSessionId: sessionId }, 'runAgent: SDK session initialized');
233
287
  continue;
234
288
  }
package/dist/cli/cron.js CHANGED
@@ -140,6 +140,10 @@ export async function cmdCronRun(jobName) {
140
140
  try {
141
141
  const response = await gateway.handleCronJob(job.name, job.prompt, job.tier, job.maxTurns, job.model, job.workDir, job.mode, job.maxHours);
142
142
  const finishedAt = new Date();
143
+ // 1.18.84: trigger source comes from the CRON_RUN_TRIGGER env var set
144
+ // by the dashboard's manual-run endpoint. Defaults to 'scheduled' for
145
+ // any other invocation path (CLI, daemon-internal callsites).
146
+ const trigger = process.env.CRON_RUN_TRIGGER || 'scheduled';
143
147
  const entry = {
144
148
  jobName: job.name,
145
149
  startedAt: startedAt.toISOString(),
@@ -148,6 +152,7 @@ export async function cmdCronRun(jobName) {
148
152
  durationMs: finishedAt.getTime() - startedAt.getTime(),
149
153
  attempt: 1,
150
154
  outputPreview: response ? response.slice(0, 200) : undefined,
155
+ trigger,
151
156
  };
152
157
  // PRD Phase 1.1: goal-orientation evaluator (mirrors the daemon path).
153
158
  if (job.successSchema || (job.successCriteriaText && job.successCriteriaText.trim())) {
@@ -170,6 +175,7 @@ export async function cmdCronRun(jobName) {
170
175
  }
171
176
  catch (err) {
172
177
  const finishedAt = new Date();
178
+ const trigger = process.env.CRON_RUN_TRIGGER || 'scheduled';
173
179
  runLog.append({
174
180
  jobName: job.name,
175
181
  startedAt: startedAt.toISOString(),
@@ -179,6 +185,7 @@ export async function cmdCronRun(jobName) {
179
185
  error: String(err).slice(0, 500),
180
186
  errorType: classifyError(err),
181
187
  attempt: 1,
188
+ trigger,
182
189
  });
183
190
  console.error(`Error: ${err}`);
184
191
  process.exit(1);
@@ -4613,7 +4613,8 @@ export async function cmdDashboard(opts) {
4613
4613
  detached: true,
4614
4614
  stdio: ['ignore', 'pipe', 'pipe'],
4615
4615
  cwd: BASE_DIR,
4616
- env: { ...process.env, CLEMENTINE_HOME: BASE_DIR },
4616
+ // 1.18.84: pass the trigger source so cron.ts stamps it on the run entry.
4617
+ env: { ...process.env, CLEMENTINE_HOME: BASE_DIR, CRON_RUN_TRIGGER: 'manual' },
4617
4618
  });
4618
4619
  // Capture stderr for error reporting
4619
4620
  let stderr = '';
@@ -16389,6 +16390,9 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
16389
16390
  <button class="build-tab-btn active" data-build-tab="crons" onclick="switchBuildTab('crons')" style="padding:8px 14px;border-radius:6px 6px 0 0;border:none;background:transparent;color:var(--text-primary);font-size:13px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent">
16390
16391
  <span style="margin-right:6px">📅</span>Tasks <span id="build-tab-cron-count" style="display:none;margin-left:4px;font-size:10px;background:var(--bg-tertiary);padding:1px 6px;border-radius:999px;color:var(--text-muted)">0</span>
16391
16392
  </button>
16393
+ <button class="build-tab-btn" data-build-tab="runs" onclick="switchBuildTab('runs')" style="padding:8px 14px;border-radius:6px 6px 0 0;border:none;background:transparent;color:var(--text-secondary);font-size:13px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent">
16394
+ <span style="margin-right:6px">🕒</span>Runs <span id="build-tab-runs-count" style="display:none;margin-left:4px;font-size:10px;background:var(--bg-tertiary);padding:1px 6px;border-radius:999px;color:var(--text-muted)">0</span>
16395
+ </button>
16392
16396
  <button class="build-tab-btn" data-build-tab="toolsmcp" onclick="switchBuildTab('toolsmcp')" style="padding:8px 14px;border-radius:6px 6px 0 0;border:none;background:transparent;color:var(--text-secondary);font-size:13px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent">
16393
16397
  <span style="margin-right:6px">🧰</span>Tools &amp; MCP <span id="build-tab-toolsmcp-count" style="display:none;margin-left:4px;font-size:10px;background:var(--bg-tertiary);padding:1px 6px;border-radius:999px;color:var(--text-muted)">0</span>
16394
16398
  </button>
@@ -16404,6 +16408,12 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
16404
16408
  <div id="build-tab-crons" style="display:none;flex:1;min-height:0;overflow-y:auto;padding:18px;background:var(--bg-primary)">
16405
16409
  <div id="panel-cron"><div class="empty-state" style="padding:24px;color:var(--text-muted)">Loading scheduled tasks…</div></div>
16406
16410
  </div>
16411
+ <!-- ── PRD Phase 3: Run list ───────────────────────────────────────────
16412
+ Single table of every run across all tasks, with filters + saved
16413
+ views. Default view is "Failures (last 24h)". -->
16414
+ <div id="build-tab-runs" style="display:none;flex:1;min-height:0;overflow-y:auto;padding:18px;background:var(--bg-primary)">
16415
+ <div id="panel-runs"><div class="empty-state" style="padding:24px;color:var(--text-muted)">Loading run history…</div></div>
16416
+ </div>
16407
16417
  <!-- ── PRD Phase 2: Tools & MCP catalog ────────────────────────────────
16408
16418
  Read-only foundation in 1.18.81. Future slices: per-tool bindings,
16409
16419
  Reconnect/Toggle/Edit actions, Approval Mode + Max-auto-runs config. -->
@@ -21261,8 +21271,10 @@ function switchBuildTab(tab) {
21261
21271
  // Always close any open workflow when changing tabs — switching context
21262
21272
  // is a clean slate, not a stale node hanging on the canvas.
21263
21273
  if (typeof closeBuilderCanvas === 'function') closeBuilderCanvas();
21264
- // Default: hide the Tools & MCP pane unless we're explicitly on it.
21274
+ var runsPane = document.getElementById('build-tab-runs');
21275
+ // Default: hide the Tools & MCP + Runs panes unless we're explicitly on them.
21265
21276
  if (toolsmcpPane && tab !== 'toolsmcp') toolsmcpPane.style.display = 'none';
21277
+ if (runsPane && tab !== 'runs') runsPane.style.display = 'none';
21266
21278
  if (tab === 'toolsmcp') {
21267
21279
  // PRD Phase 2: Tools & MCP catalog. Read-only foundation in 1.18.81.
21268
21280
  if (workPane) workPane.style.display = 'none';
@@ -21275,6 +21287,18 @@ function switchBuildTab(tab) {
21275
21287
  if (typeof refreshToolsMcpCatalog === 'function') refreshToolsMcpCatalog();
21276
21288
  return;
21277
21289
  }
21290
+ if (tab === 'runs') {
21291
+ // PRD Phase 3: Run list — every run across every task.
21292
+ if (workPane) workPane.style.display = 'none';
21293
+ if (cronPane) cronPane.style.display = 'none';
21294
+ if (tplPane) tplPane.style.display = 'none';
21295
+ if (runsPane) runsPane.style.display = 'block';
21296
+ if (headerStrip) headerStrip.style.display = 'none';
21297
+ if (usagePanel) usagePanel.style.display = 'none';
21298
+ if (newBtn) newBtn.style.display = 'none';
21299
+ if (typeof refreshRunList === 'function') refreshRunList();
21300
+ return;
21301
+ }
21278
21302
  if (tab === 'templates') {
21279
21303
  if (workPane) workPane.style.display = 'none';
21280
21304
  if (cronPane) cronPane.style.display = 'none';
@@ -23653,6 +23677,246 @@ function renderRunningCard(item) {
23653
23677
  + '</div></div>';
23654
23678
  }
23655
23679
 
23680
+ // ── PRD Phase 3: Run list ──────────────────────────────────────────────
23681
+ // Single sortable/filterable table of every CronRunEntry across all tasks.
23682
+ // Filters: status, task name, time window. Browser-local saved views.
23683
+ // Default view: "Failures (last 24h)". No new endpoints — reuses
23684
+ // /api/cron/runs (CronRunLog.readAllRecent).
23685
+
23686
+ var _runListState = {
23687
+ filterStatus: 'all', // 'all' | 'failed' | 'ok'
23688
+ filterWindow: '24h', // '24h' | '7d' | 'all'
23689
+ filterText: '', // free-text task name match
23690
+ data: [], // raw runs from /api/cron/runs
23691
+ };
23692
+
23693
+ function _runListLoadDefaultView() {
23694
+ // First-time visit: PRD §5.3 — default Saved View is "Failures (last 24h)".
23695
+ try {
23696
+ var raw = localStorage.getItem('runListView');
23697
+ if (raw) {
23698
+ var saved = JSON.parse(raw);
23699
+ _runListState.filterStatus = saved.filterStatus || 'all';
23700
+ _runListState.filterWindow = saved.filterWindow || '24h';
23701
+ _runListState.filterText = saved.filterText || '';
23702
+ return;
23703
+ }
23704
+ } catch (e) { /* ignore */ }
23705
+ // Default: failures, last 24h.
23706
+ _runListState.filterStatus = 'failed';
23707
+ _runListState.filterWindow = '24h';
23708
+ _runListState.filterText = '';
23709
+ }
23710
+
23711
+ function _runListSaveView() {
23712
+ try {
23713
+ localStorage.setItem('runListView', JSON.stringify({
23714
+ filterStatus: _runListState.filterStatus,
23715
+ filterWindow: _runListState.filterWindow,
23716
+ filterText: _runListState.filterText,
23717
+ }));
23718
+ } catch (e) { /* ignore */ }
23719
+ }
23720
+
23721
+ function _runListApplyFilters(runs) {
23722
+ var now = Date.now();
23723
+ var windowMs = _runListState.filterWindow === '24h' ? 24 * 60 * 60 * 1000
23724
+ : _runListState.filterWindow === '7d' ? 7 * 24 * 60 * 60 * 1000
23725
+ : Infinity;
23726
+ var query = (_runListState.filterText || '').trim().toLowerCase();
23727
+ return runs.filter(function(r) {
23728
+ if (_runListState.filterStatus === 'failed') {
23729
+ if (r.status !== 'error' && r.status !== 'timeout' && r.status !== 'lost') return false;
23730
+ } else if (_runListState.filterStatus === 'ok') {
23731
+ if (r.status !== 'ok') return false;
23732
+ }
23733
+ if (query && String(r.jobName || '').toLowerCase().indexOf(query) === -1) return false;
23734
+ if (windowMs !== Infinity && r.startedAt) {
23735
+ var age = now - new Date(r.startedAt).getTime();
23736
+ if (age > windowMs) return false;
23737
+ }
23738
+ return true;
23739
+ });
23740
+ }
23741
+
23742
+ async function refreshRunList() {
23743
+ var panel = document.getElementById('panel-runs');
23744
+ if (!panel) return;
23745
+ if (!_runListState.data.length) {
23746
+ _runListLoadDefaultView();
23747
+ }
23748
+ panel.innerHTML = '<div class="empty-state" style="padding:24px;color:var(--text-muted)">Loading run history…</div>';
23749
+ try {
23750
+ var r = await apiFetch('/api/cron/runs?limit=200');
23751
+ var d = await r.json();
23752
+ _runListState.data = (d && d.runs) || [];
23753
+ } catch (e) {
23754
+ panel.innerHTML = '<div class="empty-state" style="padding:24px;color:var(--red)">Failed to load runs: ' + esc(String(e)) + '</div>';
23755
+ return;
23756
+ }
23757
+ panel.innerHTML = renderRunListBody(_runListState.data);
23758
+ // Update tab count badge with total runs (not filtered count — that's
23759
+ // shown alongside the filter chips).
23760
+ var tabCount = document.getElementById('build-tab-runs-count');
23761
+ if (tabCount) {
23762
+ tabCount.textContent = _runListState.data.length;
23763
+ tabCount.style.display = _runListState.data.length > 0 ? '' : 'none';
23764
+ }
23765
+ }
23766
+
23767
+ function renderRunListBody(allRuns) {
23768
+ var filtered = _runListApplyFilters(allRuns);
23769
+ var html = '';
23770
+ // Header
23771
+ html += '<div style="margin-bottom:18px"><h2 style="margin:0 0 4px;font-size:18px;font-weight:600;color:var(--text-primary)">Runs</h2>'
23772
+ + '<div style="font-size:12px;color:var(--text-muted)">'+ filtered.length +' of '+ allRuns.length +' total runs · default view: <strong>Failures (last 24h)</strong></div></div>';
23773
+ // Filter row — saved automatically to localStorage on change.
23774
+ html += '<div style="display:flex;gap:10px;align-items:center;margin-bottom:14px;flex-wrap:wrap">';
23775
+ html += _runListChip('Status', [
23776
+ { value: 'all', label: 'All' },
23777
+ { value: 'ok', label: 'OK' },
23778
+ { value: 'failed', label: 'Failed' },
23779
+ ], 'filterStatus');
23780
+ html += _runListChip('Window', [
23781
+ { value: '24h', label: 'Last 24h' },
23782
+ { value: '7d', label: 'Last 7 days' },
23783
+ { value: 'all', label: 'All time' },
23784
+ ], 'filterWindow');
23785
+ html += '<input type="search" placeholder="Filter by task name…" value="' + esc(_runListState.filterText) + '" oninput="onRunListSearch(this.value)" style="flex:1;min-width:200px;max-width:320px;padding:6px 10px;font-size:12px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary)">';
23786
+ html += '<button class="btn-sm" onclick="resetRunListFilters()" style="font-size:11px">Reset to default</button>';
23787
+ html += '</div>';
23788
+ if (filtered.length === 0) {
23789
+ html += '<div class="empty-state" style="padding:36px 24px;text-align:center;color:var(--text-muted)"><div style="font-size:14px;margin-bottom:6px">No runs match the current filter.</div><div style="font-size:12px">Try widening the time window or clearing the task-name filter.</div></div>';
23790
+ return html;
23791
+ }
23792
+ // Table — same shape as the Recent History list on the Tasks page,
23793
+ // but sortable and with a Trigger column.
23794
+ html += '<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius)">';
23795
+ html += '<div style="display:grid;grid-template-columns:24px 24px minmax(180px,1.2fr) 90px minmax(180px,1fr) 90px auto;gap:10px;padding:8px 14px;border-bottom:1px solid var(--border);font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;font-weight:500">'
23796
+ + '<div></div><div title="Goal verdict">Goal</div><div>Task</div><div>Trigger</div><div>Started</div><div>Duration</div><div></div>'
23797
+ + '</div>';
23798
+ for (var i = 0; i < filtered.length; i++) {
23799
+ var entry = filtered[i] || {};
23800
+ var status = entry.status || 'unknown';
23801
+ var statusColor, statusIcon;
23802
+ if (status === 'ok') { statusColor = 'var(--green)'; statusIcon = '&#10003;'; }
23803
+ else if (status === 'error') { statusColor = 'var(--red)'; statusIcon = '&#10007;'; }
23804
+ else if (status === 'retried') { statusColor = 'var(--yellow)'; statusIcon = '&#8635;'; }
23805
+ else if (status === 'timeout') { statusColor = 'var(--yellow)'; statusIcon = '&#9203;'; }
23806
+ else if (status === 'lost') { statusColor = 'var(--red)'; statusIcon = '?'; }
23807
+ else if (status === 'running') { statusColor = 'var(--accent)'; statusIcon = '●'; }
23808
+ else if (status === 'skipped') { statusColor = 'var(--text-muted)'; statusIcon = '&minus;'; }
23809
+ else { statusColor = 'var(--text-muted)'; statusIcon = '&middot;'; }
23810
+ var jobName = entry.jobName || '(unknown)';
23811
+ var safeName = jsStr(jobName);
23812
+ var startedAt = entry.startedAt ? new Date(entry.startedAt) : null;
23813
+ var startedLabel = startedAt ? startedAt.toLocaleString() : '—';
23814
+ var durationLabel = entry.durationMs != null ? formatDurationMs(entry.durationMs) : '—';
23815
+ // 1.18.84: real persisted trigger field. Falls back to a heuristic for
23816
+ // pre-1.18.84 run entries that don't have the field set.
23817
+ var triggerLabel = entry.trigger || (entry.attempt > 1 ? 'retry' : 'scheduled');
23818
+ var triggerColor = entry.trigger === 'manual' ? 'var(--accent)'
23819
+ : entry.trigger === 'after' ? 'var(--purple)'
23820
+ : entry.trigger === 'discord' ? 'var(--blue)'
23821
+ : 'var(--text-muted)';
23822
+ // Goal cell
23823
+ var goalCell = '<div></div>';
23824
+ if (entry.goalCheck) {
23825
+ var gc = entry.goalCheck;
23826
+ var gIcon = gc.status === 'pass' ? '🎯' : gc.status === 'fail' ? '✗' : gc.status === 'error' ? '⚠' : '';
23827
+ var gColor = gc.status === 'pass' ? 'var(--green)' : gc.status === 'fail' ? 'var(--red)' : 'var(--yellow)';
23828
+ var gTip = gc.evaluatorReason || (Array.isArray(gc.schemaErrors) ? gc.schemaErrors.join('; ') : gc.status);
23829
+ goalCell = '<div style="color:' + gColor + ';font-size:13px;line-height:18px;text-align:center" title="' + esc(gTip) + '">' + gIcon + '</div>';
23830
+ }
23831
+ var preview = '';
23832
+ if (status === 'error' && entry.error) {
23833
+ preview = '<div style="font-size:11px;color:var(--red);margin-top:2px;word-break:break-word">' + esc(String(entry.error).slice(0, 140)) + '</div>';
23834
+ } else if (entry.outputPreview) {
23835
+ preview = '<div style="font-size:11px;color:var(--text-muted);margin-top:2px;word-break:break-word">' + esc(String(entry.outputPreview).slice(0, 120)) + '</div>';
23836
+ }
23837
+ html += '<div class="history-row" data-trace-job="' + esc(jobName) + '" style="display:grid;grid-template-columns:24px 24px minmax(180px,1.2fr) 90px minmax(180px,1fr) 90px auto;gap:10px;align-items:start;padding:8px 14px;border-bottom:1px solid var(--border);cursor:pointer">'
23838
+ + '<div style="color:' + statusColor + ';font-size:14px;line-height:18px;text-align:center" title="' + esc(status) + '">' + statusIcon + '</div>'
23839
+ + goalCell
23840
+ + '<div style="min-width:0">'
23841
+ + '<div style="font-weight:500;color:var(--text-primary);font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(jobName) + '">' + esc(jobName) + (entry.attempt > 1 ? ' · attempt ' + esc(entry.attempt) : '') + '</div>'
23842
+ + preview
23843
+ + '</div>'
23844
+ + '<div style="font-size:11px;color:' + triggerColor + ';line-height:18px">' + esc(triggerLabel) + '</div>'
23845
+ + '<div style="font-size:12px;color:var(--text-secondary);line-height:18px">' + esc(startedLabel) + '</div>'
23846
+ + '<div style="font-size:12px;color:var(--text-muted);line-height:18px">' + esc(durationLabel) + '</div>'
23847
+ + '<div style="display:flex;gap:6px;align-items:center"><button class="btn-sm" onclick="event.stopPropagation();openTraceViewer(\\x27' + safeName + '\\x27)" style="font-size:11px;padding:3px 8px">Trace</button></div>'
23848
+ + '</div>';
23849
+ }
23850
+ html += '</div>';
23851
+ return html;
23852
+ }
23853
+
23854
+ function _runListChip(label, options, stateKey) {
23855
+ var current = _runListState[stateKey];
23856
+ var html = '<span style="display:inline-flex;align-items:center;gap:4px">';
23857
+ html += '<span style="font-size:11px;color:var(--text-muted);font-weight:500;text-transform:uppercase;letter-spacing:0.04em;margin-right:2px">' + esc(label) + '</span>';
23858
+ for (var i = 0; i < options.length; i++) {
23859
+ var o = options[i];
23860
+ var active = o.value === current;
23861
+ var bg = active ? 'var(--accent)' : 'var(--bg-secondary)';
23862
+ var fg = active ? '#fff' : 'var(--text-primary)';
23863
+ html += '<button class="btn-sm" onclick="onRunListChipClick(\\x27' + jsStr(stateKey) + '\\x27,\\x27' + jsStr(o.value) + '\\x27)" style="font-size:11px;padding:4px 10px;background:' + bg + ';color:' + fg + ';border:1px solid var(--border);border-radius:999px">' + esc(o.label) + '</button>';
23864
+ }
23865
+ html += '</span>';
23866
+ return html;
23867
+ }
23868
+
23869
+ function onRunListChipClick(key, value) {
23870
+ _runListState[key] = value;
23871
+ _runListSaveView();
23872
+ var panel = document.getElementById('panel-runs');
23873
+ if (panel) panel.innerHTML = renderRunListBody(_runListState.data);
23874
+ }
23875
+
23876
+ function onRunListSearch(value) {
23877
+ _runListState.filterText = value;
23878
+ _runListSaveView();
23879
+ // Debounce-by-render: just re-render. Filtering is in-memory + cheap.
23880
+ var panel = document.getElementById('panel-runs');
23881
+ if (panel) panel.innerHTML = renderRunListBody(_runListState.data);
23882
+ }
23883
+
23884
+ function resetRunListFilters() {
23885
+ _runListState.filterStatus = 'failed';
23886
+ _runListState.filterWindow = '24h';
23887
+ _runListState.filterText = '';
23888
+ _runListSaveView();
23889
+ var panel = document.getElementById('panel-runs');
23890
+ if (panel) panel.innerHTML = renderRunListBody(_runListState.data);
23891
+ }
23892
+
23893
+ // Wire the panel's click handler so clicking anywhere on a row opens the
23894
+ // trace viewer (the row's data-trace-job attribute is what the existing
23895
+ // global panel-cron click handler reads).
23896
+ function _runListAttachClickHandler() {
23897
+ var pane = document.getElementById('build-tab-runs');
23898
+ if (!pane || pane._handlerAttached) return;
23899
+ pane.addEventListener('click', function(ev) {
23900
+ var t = ev.target;
23901
+ while (t && t !== pane) {
23902
+ if (t.dataset && t.dataset.traceJob) {
23903
+ openTraceViewer(t.dataset.traceJob);
23904
+ return;
23905
+ }
23906
+ t = t.parentElement;
23907
+ }
23908
+ });
23909
+ pane._handlerAttached = true;
23910
+ }
23911
+ // Attach once on first DOM ready — runs idempotent thanks to the flag.
23912
+ if (typeof document !== 'undefined') {
23913
+ if (document.readyState === 'complete' || document.readyState === 'interactive') {
23914
+ setTimeout(_runListAttachClickHandler, 0);
23915
+ } else {
23916
+ document.addEventListener('DOMContentLoaded', _runListAttachClickHandler);
23917
+ }
23918
+ }
23919
+
23656
23920
  // ── PRD Phase 2: Tools & MCP catalog ──────────────────────────────────
23657
23921
  // Read-only foundation in 1.18.81. Renders the four-card taxonomy:
23658
23922
  // • Built-in — Claude SDK native tools (Read/Write/Bash/etc.)
@@ -23670,7 +23934,14 @@ async function refreshToolsMcpCatalog() {
23670
23934
  try {
23671
23935
  var sR = await apiFetch('/api/mcp-status');
23672
23936
  var statusJson = await sR.json();
23673
- statusMap = statusJson || {};
23937
+ // /api/mcp-status returns { servers: [{name, status}], updatedAt }.
23938
+ // Build a name → entry lookup so renderMcpCatalogCard can probe by name.
23939
+ if (statusJson && Array.isArray(statusJson.servers)) {
23940
+ for (var si = 0; si < statusJson.servers.length; si++) {
23941
+ var entry = statusJson.servers[si];
23942
+ if (entry && entry.name) statusMap[entry.name] = entry;
23943
+ }
23944
+ }
23674
23945
  } catch (e) { /* status is optional — servers still render without it */ }
23675
23946
  try {
23676
23947
  var lR = await apiFetch('/api/mcp-servers');
@@ -35595,6 +35866,14 @@ try {
35595
35866
  if (evt.type === 'cron_complete' && evt.data && evt.data.job && typeof handleCronRunOnceComplete === 'function') {
35596
35867
  try { handleCronRunOnceComplete(evt.data.job); } catch (err) { /* non-fatal */ }
35597
35868
  }
35869
+ // PRD Phase 3: if the Runs tab is visible, refresh it too so a new
35870
+ // run appears at the top without a manual reload.
35871
+ if (currentPage === 'build' && typeof refreshRunList === 'function') {
35872
+ var runsPane = document.getElementById('build-tab-runs');
35873
+ if (runsPane && runsPane.style.display !== 'none') {
35874
+ try { refreshRunList(); } catch (err) { /* non-fatal */ }
35875
+ }
35876
+ }
35598
35877
  }
35599
35878
  // A delete on one tab should drop the card from every open dashboard
35600
35879
  // without waiting for the next poll. cron_toggled is similar but lighter.
@@ -888,7 +888,7 @@ export class CronScheduler {
888
888
  ctx.agentSlug = wf.agentSlug;
889
889
  return ctx;
890
890
  }
891
- async runJob(job) {
891
+ async runJob(job, trigger = 'scheduled') {
892
892
  const creditBlock = getBackgroundCreditBlock();
893
893
  if (creditBlock) {
894
894
  logger.warn({ job: job.name, until: creditBlock.until }, 'Cron job skipped — Claude credit block active');
@@ -1206,6 +1206,8 @@ export class CronScheduler {
1206
1206
  outputPreview: response ? response.slice(0, 200) : undefined,
1207
1207
  advisorApplied,
1208
1208
  terminalReason,
1209
+ // 1.18.84: persist the actual trigger source for the Run list filter.
1210
+ trigger,
1209
1211
  // Trick capability metadata — surfaced by the dashboard's
1210
1212
  // "ran with: …" line. Omit empty arrays to keep the JSONL light.
1211
1213
  ...(cronMetadata?.skillsApplied?.length ? { skillsApplied: cronMetadata.skillsApplied } : {}),
@@ -1268,7 +1270,8 @@ export class CronScheduler {
1268
1270
  const dependents = this.jobs.filter(j => j.after === job.name && j.enabled && !this.disabledJobs.has(j.name));
1269
1271
  for (const dep of dependents) {
1270
1272
  logger.info(`Chain: '${job.name}' succeeded — triggering '${dep.name}'`);
1271
- this.runJob(dep).catch((err) => {
1273
+ // 1.18.84: chained-after triggers carry trigger='after' for the Run list filter.
1274
+ this.runJob(dep, 'after').catch((err) => {
1272
1275
  logger.error({ err, job: dep.name }, `Chained job '${dep.name}' failed`);
1273
1276
  });
1274
1277
  }
package/dist/types.d.ts CHANGED
@@ -447,6 +447,11 @@ export interface CronRunEntry {
447
447
  allowedToolsApplied?: string[];
448
448
  /** MCP servers live for this run (post profile + trick allowlist intersection). */
449
449
  mcpServersApplied?: string[];
450
+ /** PRD §6 / 1.18.84: how this run was triggered. Persisted by the
451
+ * scheduler (cron tick / chained 'after' / manual-run endpoint /
452
+ * Discord) so the Run list can filter by source instead of guessing
453
+ * via heuristics on attempt count. */
454
+ trigger?: 'manual' | 'scheduled' | 'webhook' | 'api' | 'fork' | 'resume' | 'discord' | 'after';
450
455
  /** PRD Phase 1: did the run accomplish what it was supposed to?
451
456
  * Computed at run-end when the Task has successSchema or successCriteriaText.
452
457
  * - status='pass' both configured checks passed (or the only one configured did)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.82",
3
+ "version": "1.18.84",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",