clementine-agent 1.18.164 → 1.18.167
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/action-enforcer.js +4 -4
- package/dist/agent/advisor-rules/loader.js +19 -1
- package/dist/agent/assistant.js +18 -22
- package/dist/agent/execution-policy.d.ts +41 -0
- package/dist/agent/execution-policy.js +217 -0
- package/dist/agent/failure-fix-consumer.d.ts +1 -0
- package/dist/agent/failure-fix-consumer.js +22 -2
- package/dist/agent/hooks.js +2 -1
- package/dist/agent/insight-engine.js +3 -2
- package/dist/agent/mcp-bridge.js +1 -2
- package/dist/agent/prompt-cache.js +7 -0
- package/dist/agent/prompt-overrides/loader.js +19 -1
- package/dist/agent/run-agent-cron.d.ts +1 -2
- package/dist/agent/run-agent-cron.js +1 -1
- package/dist/agent/run-agent-heartbeat.js +5 -6
- package/dist/agent/run-agent.d.ts +19 -9
- package/dist/agent/run-agent.js +34 -17
- package/dist/agent/run-skill.d.ts +29 -11
- package/dist/agent/run-skill.js +46 -12
- package/dist/agent/schedule-registry.d.ts +1 -1
- package/dist/agent/self-improve.js +22 -0
- package/dist/agent/skill-store.d.ts +22 -8
- package/dist/agent/skill-store.js +181 -43
- package/dist/agent/team-bus.js +3 -1
- package/dist/agent/workflow-variables.js +4 -3
- package/dist/brain/adapters/pdf.js +2 -1
- package/dist/brain/llm-client.js +1 -1
- package/dist/brain/local-seed.d.ts +32 -0
- package/dist/brain/local-seed.js +101 -0
- package/dist/cli/cron.js +3 -1
- package/dist/cli/dashboard.d.ts +11 -0
- package/dist/cli/dashboard.js +1078 -436
- package/dist/cli/index.js +174 -8
- package/dist/cli/routes/digest.js +5 -3
- package/dist/cli/setup.js +3 -2
- package/dist/config.js +3 -1
- package/dist/dashboard/builder/prompt.js +8 -6
- package/dist/gateway/agent-heartbeat-manager.js +24 -0
- package/dist/gateway/cron-scheduler.js +4 -2
- package/dist/gateway/episodic-consolidation.d.ts +9 -0
- package/dist/gateway/episodic-consolidation.js +70 -10
- package/dist/gateway/failure-monitor.js +1 -1
- package/dist/gateway/fix-verification.js +10 -6
- package/dist/gateway/heartbeat-scheduler.js +6 -7
- package/dist/gateway/router.d.ts +1 -0
- package/dist/gateway/router.js +108 -5
- package/dist/gateway/turn-ledger.d.ts +5 -0
- package/dist/gateway/turn-ledger.js +5 -1
- package/dist/index.js +2 -2
- package/dist/lib/time.d.ts +32 -0
- package/dist/lib/time.js +150 -0
- package/dist/tools/brain-tools.d.ts +3 -0
- package/dist/tools/brain-tools.js +48 -5
- package/dist/tools/decision-reflection-tools.js +2 -2
- package/dist/tools/external-tools.js +3 -2
- package/dist/tools/schedule-tools.js +2 -2
- package/dist/tools/shared.js +6 -7
- package/dist/tools/skill-tools.js +163 -115
- package/dist/tools/vault-tools.js +1 -1
- package/dist/types.d.ts +14 -7
- package/package.json +1 -1
|
@@ -2,11 +2,11 @@ import { looksLikeApprovalPrompt } from './local-turn.js';
|
|
|
2
2
|
function normalize(text) {
|
|
3
3
|
return text.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
4
4
|
}
|
|
5
|
-
const ACTION_REQUEST_RE = /\b(can you|could you|would you|please|pls|i need you to|i want you to|let'?s|go ahead and|do it|handle this|take care of)\b[\s\S]{0,160}\b(send|email|message|post|publish|delete|change|update|run|execute|check|look(?:\s+into)?|diagnose|investigate|figure(?:\s+it)?\s*out|find|search|read|write|create|fix|schedule|reschedule|pull|fetch|review|tag|save|upload|download)\b/i;
|
|
6
|
-
const DIRECT_ACTION_RE = /^(send|email|message|post|publish|delete|change|update|run|execute|check|look(?:\s+into)?|diagnose|investigate|find|search|read|write|create|fix|schedule|reschedule|pull|fetch|review|tag|save|upload|download)\b/i;
|
|
5
|
+
const ACTION_REQUEST_RE = /\b(can you|can we|could you|would you|please|pls|i need you to|i want you to|i would like (?:for you )?to|i'?d like (?:for you )?to|let'?s|go ahead and|do it|handle this|take care of)\b[\s\S]{0,160}\b(send|email|message|post|publish|delete|change|update|run|execute|start|fire(?:\s+off)?|add|append|insert|check|look(?:\s+into)?|diagnose|investigate|figure(?:\s+it)?\s*out|find|search|research|scrape|collect|analyze|read|write|create|build|generate|fix|schedule|reschedule|pull|fetch|review|tag|save|upload|download)\b/i;
|
|
6
|
+
const DIRECT_ACTION_RE = /^(send|email|message|post|publish|delete|change|update|run|execute|start|fire(?:\s+off)?|add|append|insert|check|look(?:\s+into)?|diagnose|investigate|find|search|research|scrape|collect|analyze|read|write|create|build|generate|fix|schedule|reschedule|pull|fetch|review|tag|save|upload|download)\b/i;
|
|
7
7
|
const DIAGNOSTIC_RE = /\b(log|logs|crash|crashing|error|failing|failure|broken|diagnose|debug|investigate|look into|figure it out|what'?s causing|why is|why did)\b/i;
|
|
8
|
-
const DONE_CLAIM_RE = /\b(done|sent|emailed|queued|accepted|completed|finished|fixed|updated|changed|deleted|posted|published|scheduled|rescheduled|created|saved|uploaded|downloaded|tagged|checked|reviewed|found|read|ran|executed)\b/i;
|
|
9
|
-
const PROMISE_RE = /\b(i'?ll|i will|i am going to|i'?m going to|let me|i'?m checking|i'?m sending|i'?m running|i'?m looking|working on it|on it)\b[\s\S]{0,120}\b(send|email|message|post|publish|delete|change|update|run|execute|check|look|diagnose|investigate|find|search|read|write|create|fix|schedule|reschedule|pull|fetch|review|tag|save|upload|download|now)\b/i;
|
|
8
|
+
const DONE_CLAIM_RE = /\b(done|sent|emailed|queued|accepted|completed|finished|fixed|updated|changed|deleted|posted|published|scheduled|rescheduled|created|built|generated|saved|uploaded|downloaded|tagged|checked|reviewed|found|scraped|researched|analyzed|read|ran|executed|fired)\b/i;
|
|
9
|
+
const PROMISE_RE = /\b(i'?ll|i will|i am going to|i'?m going to|let me|i'?m checking|i'?m sending|i'?m running|i'?m looking|working on it|on it)\b[\s\S]{0,120}\b(send|email|message|post|publish|delete|change|update|run|execute|start|fire(?:\s+off)?|add|append|insert|check|look|diagnose|investigate|find|search|research|scrape|collect|analyze|read|write|create|build|generate|fix|schedule|reschedule|pull|fetch|review|tag|save|upload|download|now)\b/i;
|
|
10
10
|
const VACUOUS_ACK_RE = /^(got it|okay|ok|sure|perfect|sounds good|on it|will do|yep|yeah)[.! ]*$/i;
|
|
11
11
|
const BLOCKED_OR_ASKING_RE = /\b(i can'?t|i cannot|unable to|blocked|need you to|need a|need the|please provide|please send|can you send|can you share|which|what should|who should|before i|confirm|approve|good to go|okay to)\b/i;
|
|
12
12
|
const DIAGNOSTIC_DEFLECTION_RE = /\b(what are you seeing|what do you see|send (me )?the logs|share (the )?logs|provide (the )?logs|can you paste|can you send me)\b/i;
|
|
@@ -83,6 +83,7 @@ const ruleSchema = z.object({
|
|
|
83
83
|
// ── Loader ──────────────────────────────────────────────────────────
|
|
84
84
|
let cachedRules = [];
|
|
85
85
|
let watcherInstalled = false;
|
|
86
|
+
let watcher = null;
|
|
86
87
|
let watchDebounce = null;
|
|
87
88
|
function readYamlFile(filePath) {
|
|
88
89
|
try {
|
|
@@ -171,7 +172,7 @@ export function watchUserRulesDir(opts) {
|
|
|
171
172
|
mkdirSync(userDir, { recursive: true });
|
|
172
173
|
}
|
|
173
174
|
try {
|
|
174
|
-
fsWatch(userDir, () => {
|
|
175
|
+
watcher = fsWatch(userDir, () => {
|
|
175
176
|
if (watchDebounce)
|
|
176
177
|
clearTimeout(watchDebounce);
|
|
177
178
|
watchDebounce = setTimeout(() => {
|
|
@@ -183,6 +184,16 @@ export function watchUserRulesDir(opts) {
|
|
|
183
184
|
}
|
|
184
185
|
}, 250);
|
|
185
186
|
});
|
|
187
|
+
watcher.on('error', (err) => {
|
|
188
|
+
logger.warn({ err, userDir }, 'Rule watcher failed — hot reload disabled');
|
|
189
|
+
try {
|
|
190
|
+
watcher?.close();
|
|
191
|
+
}
|
|
192
|
+
catch { /* ignore */ }
|
|
193
|
+
watcher = null;
|
|
194
|
+
watcherInstalled = false;
|
|
195
|
+
});
|
|
196
|
+
watcher.unref();
|
|
186
197
|
watcherInstalled = true;
|
|
187
198
|
logger.debug({ dir: userDir }, 'Watching user rules dir for hot reload');
|
|
188
199
|
}
|
|
@@ -194,6 +205,13 @@ export function watchUserRulesDir(opts) {
|
|
|
194
205
|
export function _resetLoaderState() {
|
|
195
206
|
cachedRules = [];
|
|
196
207
|
watcherInstalled = false;
|
|
208
|
+
if (watcher) {
|
|
209
|
+
try {
|
|
210
|
+
watcher.close();
|
|
211
|
+
}
|
|
212
|
+
catch { /* ignore */ }
|
|
213
|
+
watcher = null;
|
|
214
|
+
}
|
|
197
215
|
if (watchDebounce) {
|
|
198
216
|
clearTimeout(watchDebounce);
|
|
199
217
|
watchDebounce = null;
|
package/dist/agent/assistant.js
CHANGED
|
@@ -13,7 +13,7 @@ import fs from 'node:fs';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { query as rawQuery, listSubagents, getSubagentMessages, SYSTEM_PROMPT_DYNAMIC_BOUNDARY, } from '@anthropic-ai/claude-agent-sdk';
|
|
15
15
|
import pino from 'pino';
|
|
16
|
-
import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, PROJECTS_META_FILE, CRON_PROGRESS_DIR, CRON_REFLECTIONS_DIR, BUDGET, TASK_BUDGET_TOKENS, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, claudeCodeDisableOneMillionForModel, currentOneMillionContextMode, normalizeClaudeModelForOneMillionContext, normalizeClaudeSdkOptionsForOneMillionContext, looksLikeClaudeOneMillionContextError, envSnapshot, } from '../config.js';
|
|
16
|
+
import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, PROJECTS_META_FILE, CRON_PROGRESS_DIR, CRON_REFLECTIONS_DIR, BUDGET, TASK_BUDGET_TOKENS, TIMEZONE, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, claudeCodeDisableOneMillionForModel, currentOneMillionContextMode, normalizeClaudeModelForOneMillionContext, normalizeClaudeSdkOptionsForOneMillionContext, looksLikeClaudeOneMillionContextError, envSnapshot, } from '../config.js';
|
|
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';
|
|
@@ -33,6 +33,7 @@ import { applyServiceDedup, routeToolSurface, TOOL_SURFACE_HARD_LIMIT, TOOL_SURF
|
|
|
33
33
|
import { isRestrictedToolset, toolsetAllowsLocalWrites, toolsetDisablesAllTools } from './toolsets.js';
|
|
34
34
|
import { looksLikeApprovalPrompt } from './local-turn.js';
|
|
35
35
|
import { loadClementineJson } from '../config/clementine-json.js';
|
|
36
|
+
import { dateKeyInTimeZone, formatDateInTimeZone, formatTimeInTimeZone, hourInTimeZone } from '../lib/time.js';
|
|
36
37
|
// ── Channel capabilities ────────────────────────────────────────────
|
|
37
38
|
/** Map channel label to its capabilities so the agent adapts its responses. */
|
|
38
39
|
function getChannelCapabilities(channel) {
|
|
@@ -562,22 +563,17 @@ function extractText(blocks) {
|
|
|
562
563
|
}
|
|
563
564
|
// ── Date Helpers ────────────────────────────────────────────────────
|
|
564
565
|
function formatDate(d) {
|
|
565
|
-
return d
|
|
566
|
-
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
|
|
567
|
-
});
|
|
566
|
+
return formatDateInTimeZone(d, TIMEZONE);
|
|
568
567
|
}
|
|
569
568
|
function formatTime(d) {
|
|
570
|
-
return d
|
|
569
|
+
return formatTimeInTimeZone(d, TIMEZONE);
|
|
571
570
|
}
|
|
572
571
|
/** Local-time YYYY-MM-DD (avoids UTC date mismatch late at night). */
|
|
573
572
|
function todayISO() {
|
|
574
|
-
|
|
575
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
573
|
+
return dateKeyInTimeZone(new Date(), TIMEZONE);
|
|
576
574
|
}
|
|
577
575
|
function yesterdayISO() {
|
|
578
|
-
|
|
579
|
-
d.setDate(d.getDate() - 1);
|
|
580
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
576
|
+
return dateKeyInTimeZone(new Date(Date.now() - 24 * 60 * 60 * 1000), TIMEZONE);
|
|
581
577
|
}
|
|
582
578
|
// ── Cron Output Extraction ──────────────────────────────────────────
|
|
583
579
|
/** Autonomous jobs use this sentinel to mean "completed, but do not notify the owner." */
|
|
@@ -1143,7 +1139,7 @@ Large tool outputs blow the context window and rotate your session mid-task —
|
|
|
1143
1139
|
// Skip yesterday's notes and recent conversation summaries for autonomous runs
|
|
1144
1140
|
if (!isAutonomous && !skipAmbientContext) {
|
|
1145
1141
|
if (!retrievalContext) {
|
|
1146
|
-
const hour = new Date()
|
|
1142
|
+
const hour = hourInTimeZone(new Date(), TIMEZONE);
|
|
1147
1143
|
const mentionsYesterday = this._lastUserMessage?.toLowerCase().includes('yesterday');
|
|
1148
1144
|
if (hour < 12 || mentionsYesterday) {
|
|
1149
1145
|
const yPath = path.join(DAILY_NOTES_DIR, `${yesterdayISO()}.md`);
|
|
@@ -1638,7 +1634,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1638
1634
|
|
|
1639
1635
|
- **Date:** ${formatDate(now)}
|
|
1640
1636
|
- **Time:** ${formatTime(now)}
|
|
1641
|
-
- **Timezone:** ${
|
|
1637
|
+
- **Timezone:** ${TIMEZONE}
|
|
1642
1638
|
- **Channel:** ${channel}${caps ? ` (${formatCapabilities(caps)})` : ''}
|
|
1643
1639
|
- **Model:** ${modelLabel} (${resolvedModel})
|
|
1644
1640
|
- **Vault:** ${vault}
|
|
@@ -2237,9 +2233,9 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2237
2233
|
}
|
|
2238
2234
|
}
|
|
2239
2235
|
}
|
|
2240
|
-
//
|
|
2241
|
-
//
|
|
2242
|
-
const effectivePermissionMode = '
|
|
2236
|
+
// Headless daemon runs cannot prompt for approval. `dontAsk` auto-allows
|
|
2237
|
+
// only the explicit allowedTools surface and denies everything else.
|
|
2238
|
+
const effectivePermissionMode = 'dontAsk';
|
|
2243
2239
|
// SessionStore adapter: mirror SDK transcripts into our SQLite store.
|
|
2244
2240
|
// Resume then works from the durable store, not just local JSONL.
|
|
2245
2241
|
const sessionStore = this.getSessionStore();
|
|
@@ -2313,7 +2309,6 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2313
2309
|
...(fallback ? { fallbackModel: fallback } : {}),
|
|
2314
2310
|
...(oneMillionDisableValue === '1' ? { betas: [] } : {}),
|
|
2315
2311
|
permissionMode: effectivePermissionMode,
|
|
2316
|
-
allowDangerouslySkipPermissions: true,
|
|
2317
2312
|
...(sessionStore ? { sessionStore } : {}),
|
|
2318
2313
|
...(computedTaskBudget && supportsTaskBudget ? { taskBudget: { total: computedTaskBudget } } : {}),
|
|
2319
2314
|
// SDK field semantics (per node_modules/@anthropic-ai/claude-agent-sdk/sdk.d.ts):
|
|
@@ -2323,8 +2318,8 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2323
2318
|
// CLEMENTINE_TOOL_ALLOWLIST so unneeded internal tools
|
|
2324
2319
|
// are never registered for lightweight turns.
|
|
2325
2320
|
// - `allowedTools` → auto-allow list covering both built-ins AND MCP tool
|
|
2326
|
-
// names
|
|
2327
|
-
//
|
|
2321
|
+
// names. With permissionMode=dontAsk, anything not
|
|
2322
|
+
// present here is denied instead of prompting.
|
|
2328
2323
|
// - `disallowedTools` → blocklist, takes precedence.
|
|
2329
2324
|
tools: toolsDisabledForCall ? [] : allowedTools.filter(t => !t.startsWith('mcp__')),
|
|
2330
2325
|
allowedTools: toolsDisabledForCall ? [] : allowedTools,
|
|
@@ -2831,8 +2826,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2831
2826
|
options: {
|
|
2832
2827
|
systemPrompt: 'You are a silent memory extraction agent. Save facts to the vault and exit.',
|
|
2833
2828
|
model: AUTO_MEMORY_MODEL,
|
|
2834
|
-
permissionMode: '
|
|
2835
|
-
allowDangerouslySkipPermissions: true,
|
|
2829
|
+
permissionMode: 'dontAsk',
|
|
2836
2830
|
// MCP tool names live in allowedTools, not tools. See note at
|
|
2837
2831
|
// buildOptions — `tools` is for built-ins only.
|
|
2838
2832
|
tools: [],
|
|
@@ -2862,6 +2856,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2862
2856
|
CLEMENTINE_TEAM_AGENT: profile?.slug ?? 'clementine',
|
|
2863
2857
|
// Auto-memory extractor runs autonomously.
|
|
2864
2858
|
CLEMENTINE_INTERACTION_SOURCE: 'autonomous',
|
|
2859
|
+
CLEMENTINE_TOOL_ALLOWLIST: 'memory_write,memory_search,note_create,task_add,note_take,memory_read,user_model',
|
|
2865
2860
|
},
|
|
2866
2861
|
},
|
|
2867
2862
|
},
|
|
@@ -3082,8 +3077,9 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3082
3077
|
options: {
|
|
3083
3078
|
systemPrompt: 'You are a task output verifier. Assess the output quality.',
|
|
3084
3079
|
model: MODELS.haiku,
|
|
3085
|
-
permissionMode: '
|
|
3086
|
-
|
|
3080
|
+
permissionMode: 'dontAsk',
|
|
3081
|
+
tools: [],
|
|
3082
|
+
allowedTools: [],
|
|
3087
3083
|
maxTurns: 1,
|
|
3088
3084
|
cwd: BASE_DIR,
|
|
3089
3085
|
env: SAFE_ENV,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Claude Agent SDK execution policy.
|
|
3
|
+
*
|
|
4
|
+
* The SDK permission model has two separate layers:
|
|
5
|
+
* - `tools` controls which built-in tools are visible.
|
|
6
|
+
* - `allowedTools` pre-approves built-ins and MCP tools.
|
|
7
|
+
*
|
|
8
|
+
* Critically, `allowedTools` does not constrain `bypassPermissions`.
|
|
9
|
+
* Headless Clementine runs should therefore default to `dontAsk` and
|
|
10
|
+
* explicitly allow the built-ins/MCP servers they need.
|
|
11
|
+
*/
|
|
12
|
+
export type ExecutionPermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'dontAsk' | 'auto';
|
|
13
|
+
export declare const SDK_BUILTIN_TOOLS: Set<string>;
|
|
14
|
+
export interface BuildExecutionPolicyOptions {
|
|
15
|
+
/** Caller-provided allowedTools. Undefined means use defaults; [] means deny all. */
|
|
16
|
+
requestedTools?: string[];
|
|
17
|
+
/** Built-ins used when requestedTools is undefined. */
|
|
18
|
+
defaultBuiltins: readonly string[];
|
|
19
|
+
/** Names of MCP servers mounted for this query. */
|
|
20
|
+
mcpServerNames: string[];
|
|
21
|
+
/** Clementine's own MCP server name, e.g. "clementine-tools". */
|
|
22
|
+
clementineServerName: string;
|
|
23
|
+
/** Optional override. Defaults to dontAsk for headless auto-execution. */
|
|
24
|
+
permissionMode?: ExecutionPermissionMode;
|
|
25
|
+
}
|
|
26
|
+
export interface ExecutionToolPolicy {
|
|
27
|
+
permissionMode: ExecutionPermissionMode;
|
|
28
|
+
allowDangerouslySkipPermissions?: true;
|
|
29
|
+
/** SDK `tools` option: visible built-ins only. */
|
|
30
|
+
builtinTools: string[];
|
|
31
|
+
/** SDK `allowedTools` option: built-ins + MCP permissions. */
|
|
32
|
+
allowedTools: string[];
|
|
33
|
+
/** Value for CLEMENTINE_TOOL_ALLOWLIST inside the Clementine MCP subprocess. */
|
|
34
|
+
clementineToolAllowlist: string;
|
|
35
|
+
/** Env vars for the SDK subprocess. */
|
|
36
|
+
env: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
/** Best-effort registry of first-party Clementine MCP tool names. */
|
|
39
|
+
export declare function listClementineMcpToolNames(): Set<string>;
|
|
40
|
+
export declare function buildExecutionToolPolicy(opts: BuildExecutionPolicyOptions): ExecutionToolPolicy;
|
|
41
|
+
//# sourceMappingURL=execution-policy.d.ts.map
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Claude Agent SDK execution policy.
|
|
3
|
+
*
|
|
4
|
+
* The SDK permission model has two separate layers:
|
|
5
|
+
* - `tools` controls which built-in tools are visible.
|
|
6
|
+
* - `allowedTools` pre-approves built-ins and MCP tools.
|
|
7
|
+
*
|
|
8
|
+
* Critically, `allowedTools` does not constrain `bypassPermissions`.
|
|
9
|
+
* Headless Clementine runs should therefore default to `dontAsk` and
|
|
10
|
+
* explicitly allow the built-ins/MCP servers they need.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { BASE_DIR, PKG_DIR } from '../config.js';
|
|
15
|
+
export const SDK_BUILTIN_TOOLS = new Set([
|
|
16
|
+
'Agent',
|
|
17
|
+
'AskUserQuestion',
|
|
18
|
+
'Bash',
|
|
19
|
+
'Edit',
|
|
20
|
+
'Glob',
|
|
21
|
+
'Grep',
|
|
22
|
+
'Monitor',
|
|
23
|
+
'Read',
|
|
24
|
+
'TodoWrite',
|
|
25
|
+
'WebFetch',
|
|
26
|
+
'WebSearch',
|
|
27
|
+
'Write',
|
|
28
|
+
]);
|
|
29
|
+
let _cachedClementineToolNames = null;
|
|
30
|
+
function discoverToolNamesInDir(dir, out) {
|
|
31
|
+
if (!existsSync(dir))
|
|
32
|
+
return;
|
|
33
|
+
let files = [];
|
|
34
|
+
try {
|
|
35
|
+
files = readdirSync(dir).filter((f) => /\.(ts|js|mjs)$/.test(f));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const re = /\.tool\(\s*['"`]([A-Za-z0-9_.:-]+)['"`]/g;
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
let text = '';
|
|
43
|
+
try {
|
|
44
|
+
text = readFileSync(path.join(dir, file), 'utf-8');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
re.lastIndex = 0;
|
|
50
|
+
let m;
|
|
51
|
+
while ((m = re.exec(text)) !== null)
|
|
52
|
+
out.add(m[1]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function discoverUserToolNames(dir, out) {
|
|
56
|
+
if (!existsSync(dir))
|
|
57
|
+
return;
|
|
58
|
+
let files = [];
|
|
59
|
+
try {
|
|
60
|
+
files = readdirSync(dir).filter((f) => /\.(sh|py)$/.test(f));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
const name = file.replace(/\.(sh|py)$/, '').replace(/[^a-z0-9_]/gi, '_');
|
|
67
|
+
if (name)
|
|
68
|
+
out.add(name);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Best-effort registry of first-party Clementine MCP tool names. */
|
|
72
|
+
export function listClementineMcpToolNames() {
|
|
73
|
+
if (_cachedClementineToolNames)
|
|
74
|
+
return _cachedClementineToolNames;
|
|
75
|
+
const out = new Set();
|
|
76
|
+
discoverToolNamesInDir(path.join(PKG_DIR, 'src', 'tools'), out);
|
|
77
|
+
discoverToolNamesInDir(path.join(PKG_DIR, 'dist', 'tools'), out);
|
|
78
|
+
discoverUserToolNames(path.join(BASE_DIR, 'tools'), out);
|
|
79
|
+
_cachedClementineToolNames = out;
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
function mcpWildcard(serverName) {
|
|
83
|
+
return `mcp__${serverName}__*`;
|
|
84
|
+
}
|
|
85
|
+
function clementineMcpTool(serverName, toolName) {
|
|
86
|
+
return `mcp__${serverName}__${toolName}`;
|
|
87
|
+
}
|
|
88
|
+
function commandExistsOnPath(command) {
|
|
89
|
+
if (!/^[A-Za-z0-9._-]+$/.test(command))
|
|
90
|
+
return false;
|
|
91
|
+
const pathValue = process.env.PATH ?? '';
|
|
92
|
+
for (const dir of pathValue.split(path.delimiter)) {
|
|
93
|
+
if (!dir)
|
|
94
|
+
continue;
|
|
95
|
+
if (existsSync(path.join(dir, command)))
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
function normalizeRequestedToolName(raw, clementineServerName, clementineTools) {
|
|
101
|
+
const tool = String(raw ?? '').trim();
|
|
102
|
+
if (!tool)
|
|
103
|
+
return { sdkTool: '' };
|
|
104
|
+
if (/^Bash\(.+\)$/.test(tool)) {
|
|
105
|
+
return { sdkTool: tool, builtinTool: 'Bash', scopedBash: tool !== 'Bash(*)' };
|
|
106
|
+
}
|
|
107
|
+
if (tool.startsWith('mcp__')) {
|
|
108
|
+
const prefix = `mcp__${clementineServerName}__`;
|
|
109
|
+
if (tool === mcpWildcard(clementineServerName)) {
|
|
110
|
+
return { sdkTool: tool, clementineTool: '*' };
|
|
111
|
+
}
|
|
112
|
+
if (tool.startsWith(prefix)) {
|
|
113
|
+
const name = tool.slice(prefix.length);
|
|
114
|
+
if (name && name !== '*')
|
|
115
|
+
return { sdkTool: tool, clementineTool: name };
|
|
116
|
+
}
|
|
117
|
+
return { sdkTool: tool };
|
|
118
|
+
}
|
|
119
|
+
if (SDK_BUILTIN_TOOLS.has(tool))
|
|
120
|
+
return { sdkTool: tool, builtinTool: tool };
|
|
121
|
+
if (clementineTools.has(tool)) {
|
|
122
|
+
return { sdkTool: clementineMcpTool(clementineServerName, tool), clementineTool: tool };
|
|
123
|
+
}
|
|
124
|
+
if (commandExistsOnPath(tool)) {
|
|
125
|
+
return { sdkTool: `Bash(${tool}:*)`, builtinTool: 'Bash', scopedBash: true };
|
|
126
|
+
}
|
|
127
|
+
// Unknown names are preserved for forward compatibility with SDK/plugins.
|
|
128
|
+
return { sdkTool: tool };
|
|
129
|
+
}
|
|
130
|
+
function applyNormalizedTool(normalized, builtins, allowed, clementineAllow, opts = {}) {
|
|
131
|
+
const { sdkTool, clementineTool, builtinTool } = normalized;
|
|
132
|
+
if (!sdkTool)
|
|
133
|
+
return;
|
|
134
|
+
if (builtinTool && SDK_BUILTIN_TOOLS.has(builtinTool))
|
|
135
|
+
builtins.add(builtinTool);
|
|
136
|
+
if (!(opts.skipBroadBash && sdkTool === 'Bash'))
|
|
137
|
+
allowed.add(sdkTool);
|
|
138
|
+
if (clementineTool === '*') {
|
|
139
|
+
clementineAllow.clear();
|
|
140
|
+
clementineAllow.add('*');
|
|
141
|
+
}
|
|
142
|
+
else if (clementineTool && !clementineAllow.has('*')) {
|
|
143
|
+
clementineAllow.add(clementineTool);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function applyMemoryCompanionTools(normalizedTools, clementineTools, clementineServerName, builtins, allowed, clementineAllow) {
|
|
147
|
+
if (clementineAllow.has('*'))
|
|
148
|
+
return;
|
|
149
|
+
const requestedClementine = new Set(normalizedTools
|
|
150
|
+
.map(tool => tool.clementineTool)
|
|
151
|
+
.filter((tool) => Boolean(tool) && tool !== '*'));
|
|
152
|
+
if (!requestedClementine.has('memory_write'))
|
|
153
|
+
return;
|
|
154
|
+
// Existing agent profiles predate the brain ingestion tools. If a profile
|
|
155
|
+
// already grants durable memory writes, keep the newer ingestion write path
|
|
156
|
+
// available without widening to the full Clementine MCP surface.
|
|
157
|
+
const companionTools = ['brain_save'];
|
|
158
|
+
const canReadLocalData = normalizedTools.some(tool => tool.builtinTool === 'Read' || tool.builtinTool === 'Bash');
|
|
159
|
+
if (canReadLocalData)
|
|
160
|
+
companionTools.push('brain_ingest_folder');
|
|
161
|
+
for (const toolName of companionTools) {
|
|
162
|
+
if (!clementineTools.has(toolName) || requestedClementine.has(toolName))
|
|
163
|
+
continue;
|
|
164
|
+
const normalized = normalizeRequestedToolName(toolName, clementineServerName, clementineTools);
|
|
165
|
+
applyNormalizedTool(normalized, builtins, allowed, clementineAllow);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export function buildExecutionToolPolicy(opts) {
|
|
169
|
+
const permissionMode = opts.permissionMode ?? 'dontAsk';
|
|
170
|
+
const clementineTools = listClementineMcpToolNames();
|
|
171
|
+
const allowed = new Set();
|
|
172
|
+
const builtins = new Set();
|
|
173
|
+
const clementineAllow = new Set();
|
|
174
|
+
if (opts.requestedTools === undefined) {
|
|
175
|
+
for (const raw of opts.defaultBuiltins) {
|
|
176
|
+
const normalized = normalizeRequestedToolName(raw, opts.clementineServerName, clementineTools);
|
|
177
|
+
applyNormalizedTool(normalized, builtins, allowed, clementineAllow);
|
|
178
|
+
}
|
|
179
|
+
for (const server of opts.mcpServerNames) {
|
|
180
|
+
if (!server)
|
|
181
|
+
continue;
|
|
182
|
+
allowed.add(mcpWildcard(server));
|
|
183
|
+
if (server === opts.clementineServerName)
|
|
184
|
+
clementineAllow.add('*');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
const normalizedTools = opts.requestedTools.map((raw) => normalizeRequestedToolName(raw, opts.clementineServerName, clementineTools));
|
|
189
|
+
const hasScopedBash = normalizedTools.some((tool) => tool.scopedBash);
|
|
190
|
+
for (const normalized of normalizedTools) {
|
|
191
|
+
applyNormalizedTool(normalized, builtins, allowed, clementineAllow, {
|
|
192
|
+
// In explicit skill/tool scopes, `Bash` plus `some-cli` usually means
|
|
193
|
+
// "make Bash visible and approve that CLI", not "approve all shell".
|
|
194
|
+
skipBroadBash: hasScopedBash,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
if (opts.requestedTools.length > 0) {
|
|
198
|
+
applyMemoryCompanionTools(normalizedTools, clementineTools, opts.clementineServerName, builtins, allowed, clementineAllow);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const clementineToolAllowlist = clementineAllow.has('*')
|
|
202
|
+
? '*'
|
|
203
|
+
: [...clementineAllow].sort().join(',');
|
|
204
|
+
return {
|
|
205
|
+
permissionMode,
|
|
206
|
+
...(permissionMode === 'bypassPermissions' ? { allowDangerouslySkipPermissions: true } : {}),
|
|
207
|
+
builtinTools: [...builtins].sort(),
|
|
208
|
+
allowedTools: [...allowed].sort(),
|
|
209
|
+
clementineToolAllowlist,
|
|
210
|
+
env: {
|
|
211
|
+
// Tool search is default in current SDKs, but setting this makes the
|
|
212
|
+
// intent explicit and protects large Composio/MCP catalogs.
|
|
213
|
+
ENABLE_TOOL_SEARCH: process.env.ENABLE_TOOL_SEARCH || 'auto:5',
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=execution-policy.js.map
|
|
@@ -114,6 +114,7 @@ export declare class FailureFixConsumer {
|
|
|
114
114
|
private ticking;
|
|
115
115
|
constructor(dispatcher: SelfImproveDispatcher, opts?: SelfImproveLoopOptions);
|
|
116
116
|
start(): void;
|
|
117
|
+
private startFallbackTimer;
|
|
117
118
|
stop(): void;
|
|
118
119
|
/** Coalesce a burst of fs.watch events (multiple triggers landing in
|
|
119
120
|
* quick succession) into a single tick. */
|
|
@@ -43,6 +43,7 @@ const logger = pino({ name: 'clementine.failure-fix-consumer' });
|
|
|
43
43
|
const FALLBACK_TICK_MS = 60 * 60 * 1000;
|
|
44
44
|
/** Coalesce a burst of fs.watch events into a single tick. */
|
|
45
45
|
const WATCH_DEBOUNCE_MS = 2000;
|
|
46
|
+
const WATCH_FAILURE_FALLBACK_MS = 1000;
|
|
46
47
|
const TRIGGERS_DIR = path.join(BASE_DIR, 'self-improve', 'triggers');
|
|
47
48
|
const PENDING_CHANGES_DIR = path.join(BASE_DIR, 'self-improve', 'pending-changes');
|
|
48
49
|
const CRON_PATH = path.join(SYSTEM_DIR, 'CRON.md');
|
|
@@ -274,16 +275,35 @@ export class FailureFixConsumer {
|
|
|
274
275
|
return;
|
|
275
276
|
this.scheduleDebouncedTick();
|
|
276
277
|
});
|
|
278
|
+
this.watcher.on('error', (err) => {
|
|
279
|
+
logger.warn({ err, dir: this.triggersDir }, 'Triggers dir watcher failed — falling back to polling');
|
|
280
|
+
try {
|
|
281
|
+
this.watcher?.close();
|
|
282
|
+
}
|
|
283
|
+
catch { /* ignore */ }
|
|
284
|
+
this.watcher = null;
|
|
285
|
+
this.startFallbackTimer(Math.min(this.tickMs, WATCH_FAILURE_FALLBACK_MS));
|
|
286
|
+
this.scheduleDebouncedTick();
|
|
287
|
+
});
|
|
277
288
|
}
|
|
278
289
|
catch (err) {
|
|
279
290
|
logger.warn({ err, dir: this.triggersDir }, 'Failed to watch triggers dir — falling back to polling only');
|
|
291
|
+
this.startFallbackTimer(Math.min(this.tickMs, WATCH_FAILURE_FALLBACK_MS));
|
|
280
292
|
}
|
|
281
293
|
}
|
|
282
294
|
// Slow fallback safety net — covers fs.watch event drops + boot-with-backlog.
|
|
295
|
+
if (!this.timer)
|
|
296
|
+
this.startFallbackTimer(this.tickMs);
|
|
297
|
+
logger.info({ fallbackTickMs: this.tickMs, watchEnabled: this.watchEnabled && this.watcher !== null }, 'Self-improve loop started');
|
|
298
|
+
}
|
|
299
|
+
startFallbackTimer(intervalMs) {
|
|
300
|
+
if (!this.running || !Number.isFinite(intervalMs) || intervalMs <= 0)
|
|
301
|
+
return;
|
|
302
|
+
if (this.timer)
|
|
303
|
+
clearInterval(this.timer);
|
|
283
304
|
this.timer = setInterval(() => {
|
|
284
305
|
this.tick().catch((err) => logger.error({ err }, 'Self-improve fallback tick failed'));
|
|
285
|
-
},
|
|
286
|
-
logger.info({ fallbackTickMs: this.tickMs, watchEnabled: this.watchEnabled && this.watcher !== null }, 'Self-improve loop started');
|
|
306
|
+
}, intervalMs);
|
|
287
307
|
}
|
|
288
308
|
stop() {
|
|
289
309
|
if (!this.running)
|
package/dist/agent/hooks.js
CHANGED
|
@@ -13,6 +13,7 @@ import path from 'node:path';
|
|
|
13
13
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
14
14
|
import { randomUUID } from 'node:crypto';
|
|
15
15
|
import { OWNER_NAME, BASE_DIR, TIMEZONE } from '../config.js';
|
|
16
|
+
import { formatTime24InTimeZone } from '../lib/time.js';
|
|
16
17
|
// ── Shared state ───────────────────────────────────────────────────────
|
|
17
18
|
let heartbeatActive = false;
|
|
18
19
|
let heartbeatTier2Allowed = false;
|
|
@@ -189,7 +190,7 @@ export function clearActiveQueryContext() {
|
|
|
189
190
|
// not by us. setInteractionSource resets it in the relevant transitions.
|
|
190
191
|
}
|
|
191
192
|
export function logToolUse(toolName, toolInput) {
|
|
192
|
-
const timestamp = new Date()
|
|
193
|
+
const timestamp = formatTime24InTimeZone(new Date(), TIMEZONE);
|
|
193
194
|
const summary = summarizeToolCall(toolName, toolInput);
|
|
194
195
|
const entry = `- \`${timestamp}\` **${toolName}** — ${summary}`;
|
|
195
196
|
auditLog.push(entry);
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import pino from 'pino';
|
|
14
|
-
import { GOALS_DIR, BASE_DIR, MEMORY_DB_PATH, VAULT_DIR } from '../config.js';
|
|
14
|
+
import { GOALS_DIR, BASE_DIR, MEMORY_DB_PATH, VAULT_DIR, TIMEZONE } from '../config.js';
|
|
15
15
|
import { listAllGoals } from '../tools/shared.js';
|
|
16
16
|
import { computeBrokenJobs } from '../gateway/failure-monitor.js';
|
|
17
17
|
import { MemoryStore } from '../memory/store.js';
|
|
18
|
+
import { dateKeyInTimeZone } from '../lib/time.js';
|
|
18
19
|
const logger = pino({ name: 'clementine.insight-engine' });
|
|
19
20
|
const BASE_COOLDOWN_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
20
21
|
const MAX_DAILY_INSIGHTS = 3;
|
|
@@ -23,7 +24,7 @@ const UNACKED_THRESHOLD = 3; // double cooldown after this many ignored
|
|
|
23
24
|
* Check if it's too soon to send another proactive message.
|
|
24
25
|
*/
|
|
25
26
|
export function canSendInsight(state) {
|
|
26
|
-
const today = new Date()
|
|
27
|
+
const today = dateKeyInTimeZone(new Date(), TIMEZONE);
|
|
27
28
|
// Reset daily count on new day
|
|
28
29
|
if (state.currentDate !== today) {
|
|
29
30
|
state.sentToday = [];
|
package/dist/agent/mcp-bridge.js
CHANGED
|
@@ -453,8 +453,7 @@ export async function probeAvailableTools(force = false) {
|
|
|
453
453
|
options: normalizeClaudeSdkOptionsForOneMillionContext({
|
|
454
454
|
systemPrompt: 'Reply ok.',
|
|
455
455
|
model: 'claude-haiku-4-5',
|
|
456
|
-
permissionMode: '
|
|
457
|
-
allowDangerouslySkipPermissions: true,
|
|
456
|
+
permissionMode: 'dontAsk',
|
|
458
457
|
mcpServers: externalMcpServers,
|
|
459
458
|
}),
|
|
460
459
|
});
|
|
@@ -24,6 +24,13 @@ export class PromptCache {
|
|
|
24
24
|
return;
|
|
25
25
|
try {
|
|
26
26
|
const w = watch(filePath, () => { this.cache.delete(filePath); });
|
|
27
|
+
w.on('error', () => {
|
|
28
|
+
try {
|
|
29
|
+
w.close();
|
|
30
|
+
}
|
|
31
|
+
catch { /* ignore */ }
|
|
32
|
+
this.watchers.delete(filePath);
|
|
33
|
+
});
|
|
27
34
|
w.unref(); // Don't keep daemon alive
|
|
28
35
|
this.watchers.set(filePath, w);
|
|
29
36
|
}
|
|
@@ -20,6 +20,7 @@ function rootDir(baseDir) {
|
|
|
20
20
|
// ── State ────────────────────────────────────────────────────────────
|
|
21
21
|
let cached = [];
|
|
22
22
|
let watcherInstalled = false;
|
|
23
|
+
let watcher = null;
|
|
23
24
|
let watchDebounce = null;
|
|
24
25
|
// ── Parse one file ───────────────────────────────────────────────────
|
|
25
26
|
function parseOverride(filePath, scope, scopeKey) {
|
|
@@ -132,7 +133,7 @@ export function watchPromptOverrides(opts) {
|
|
|
132
133
|
mkdirSync(p, { recursive: true });
|
|
133
134
|
}
|
|
134
135
|
try {
|
|
135
|
-
fsWatch(root, { recursive: true }, () => {
|
|
136
|
+
watcher = fsWatch(root, { recursive: true }, () => {
|
|
136
137
|
if (watchDebounce)
|
|
137
138
|
clearTimeout(watchDebounce);
|
|
138
139
|
watchDebounce = setTimeout(() => {
|
|
@@ -144,6 +145,16 @@ export function watchPromptOverrides(opts) {
|
|
|
144
145
|
}
|
|
145
146
|
}, 250);
|
|
146
147
|
});
|
|
148
|
+
watcher.on('error', (err) => {
|
|
149
|
+
logger.warn({ err, root }, 'Prompt-overrides watcher failed — hot reload disabled');
|
|
150
|
+
try {
|
|
151
|
+
watcher?.close();
|
|
152
|
+
}
|
|
153
|
+
catch { /* ignore */ }
|
|
154
|
+
watcher = null;
|
|
155
|
+
watcherInstalled = false;
|
|
156
|
+
});
|
|
157
|
+
watcher.unref();
|
|
147
158
|
watcherInstalled = true;
|
|
148
159
|
logger.debug({ root }, 'Watching prompt-overrides for hot reload');
|
|
149
160
|
}
|
|
@@ -155,6 +166,13 @@ export function watchPromptOverrides(opts) {
|
|
|
155
166
|
export function _resetLoaderState() {
|
|
156
167
|
cached = [];
|
|
157
168
|
watcherInstalled = false;
|
|
169
|
+
if (watcher) {
|
|
170
|
+
try {
|
|
171
|
+
watcher.close();
|
|
172
|
+
}
|
|
173
|
+
catch { /* ignore */ }
|
|
174
|
+
watcher = null;
|
|
175
|
+
}
|
|
158
176
|
if (watchDebounce) {
|
|
159
177
|
clearTimeout(watchDebounce);
|
|
160
178
|
watchDebounce = null;
|
|
@@ -210,8 +210,7 @@ export interface RunAgentCronResult extends RunAgentResult {
|
|
|
210
210
|
/** Pinned skills that didn't resolve (bad slug / suppressed). Empty array
|
|
211
211
|
* is fine; only populated when the trick had pins that failed to load. */
|
|
212
212
|
skillsMissing: string[];
|
|
213
|
-
/** Effective
|
|
214
|
-
* means the trick didn't override — runAgent fell through to profile/default. */
|
|
213
|
+
/** Effective SDK allowedTools after defaults/MCP mapping were applied. */
|
|
215
214
|
allowedToolsApplied?: string[];
|
|
216
215
|
/** MCP servers live for this run after profile + trick intersection. */
|
|
217
216
|
mcpServersApplied: string[];
|
|
@@ -809,7 +809,7 @@ export async function runAgentCron(opts) {
|
|
|
809
809
|
externalConnected,
|
|
810
810
|
skillsApplied: plan.skillsApplied,
|
|
811
811
|
skillsMissing: plan.skillsMissing,
|
|
812
|
-
allowedToolsApplied: effectiveAllowedTools,
|
|
812
|
+
allowedToolsApplied: result.allowedToolsApplied ?? effectiveAllowedTools,
|
|
813
813
|
mcpServersApplied,
|
|
814
814
|
};
|
|
815
815
|
}
|