clementine-agent 1.18.208 → 1.18.210
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/agent-definitions.js +10 -10
- package/dist/agent/approval-signals.d.ts +2 -2
- package/dist/agent/approval-signals.js +1 -1
- package/dist/agent/assistant.js +34 -3
- package/dist/agent/bg-planner.d.ts +3 -4
- package/dist/agent/bg-planner.js +4 -5
- package/dist/agent/claim-verification.d.ts +1 -1
- package/dist/agent/claim-verification.js +1 -1
- package/dist/agent/clarification-gate.d.ts +67 -0
- package/dist/agent/clarification-gate.js +271 -0
- package/dist/agent/clementine-turn-context.d.ts +1 -1
- package/dist/agent/clementine-turn-context.js +8 -3
- package/dist/agent/complex-task-detector.d.ts +7 -12
- package/dist/agent/complex-task-detector.js +70 -26
- package/dist/agent/execution-policy.d.ts +23 -0
- package/dist/agent/execution-policy.js +36 -0
- package/dist/agent/intent-classifier.d.ts +1 -1
- package/dist/agent/project-resolver.d.ts +3 -3
- package/dist/agent/project-resolver.js +7 -7
- package/dist/agent/role-scaffolds.d.ts +1 -1
- package/dist/agent/role-scaffolds.js +1 -1
- package/dist/agent/run-agent-context.js +4 -5
- package/dist/agent/run-agent-cron.d.ts +2 -3
- package/dist/agent/run-agent-cron.js +7 -3
- package/dist/agent/run-agent-heartbeat.js +6 -2
- package/dist/agent/run-agent-mcp.d.ts +1 -2
- package/dist/agent/run-agent-mcp.js +29 -5
- package/dist/agent/run-agent-team-task.js +6 -3
- package/dist/agent/run-agent.js +2 -2
- package/dist/agent/run-summary.js +23 -4
- package/dist/agent/schedule-registry.d.ts +2 -2
- package/dist/agent/side-effect-classifier.js +18 -0
- package/dist/agent/skill-suppressions.d.ts +1 -1
- package/dist/agent/skill-suppressions.js +1 -1
- package/dist/agent/turn-policy.js +3 -3
- package/dist/channels/discord-agent-bot.js +2 -12
- package/dist/channels/discord-utils.d.ts +2 -0
- package/dist/channels/discord-utils.js +32 -0
- package/dist/channels/discord.js +2 -12
- package/dist/cli/dashboard.js +7 -6
- package/dist/config.js +4 -3
- package/dist/gateway/failure-monitor.js +9 -2
- package/dist/gateway/heartbeat-scheduler.js +1 -1
- package/dist/gateway/router.d.ts +1 -0
- package/dist/gateway/router.js +101 -30
- package/dist/integrations/composio/client.d.ts +6 -0
- package/dist/integrations/composio/client.js +117 -10
- package/dist/memory/store.js +2 -2
- package/dist/tools/admin-tools.js +2 -2
- package/dist/tools/project-tools.d.ts +1 -1
- package/dist/tools/project-tools.js +68 -17
- package/dist/tools/schedule-tools.js +1 -1
- package/dist/vault-migrations/0002-add-agentic-communication.js +1 -1
- package/package.json +1 -1
|
@@ -141,15 +141,15 @@ function buildHiredAgentDescription(p) {
|
|
|
141
141
|
'Spawn this subagent when the user names them, asks a question in their domain, or asks Clementine to "have <name> do X".',
|
|
142
142
|
].filter(Boolean).join(' ');
|
|
143
143
|
}
|
|
144
|
-
/** Map a hired-agent profile to an AgentDefinition.
|
|
145
|
-
* Used when Clementine wants to delegate to Ross/Sasha/Nora etc. */
|
|
144
|
+
/** Map a hired-agent profile to an AgentDefinition. */
|
|
146
145
|
function profileToAgentDefinition(p) {
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
146
|
+
// Hired-agent definitions are leaf subagents by default. The Claude
|
|
147
|
+
// Agent SDK does not support recursive subagent spawning via `Agent`
|
|
148
|
+
// inside subagent tool lists; if Clementine grows nested orchestration,
|
|
149
|
+
// it should be explicit and depth-limited rather than accidental.
|
|
150
|
+
const baseline = ['Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch', 'TodoWrite'];
|
|
151
151
|
const tools = p.team?.allowedTools?.length
|
|
152
|
-
? Array.from(new Set(
|
|
152
|
+
? Array.from(new Set(p.team.allowedTools.filter(tool => tool !== 'Agent')))
|
|
153
153
|
: baseline;
|
|
154
154
|
return {
|
|
155
155
|
description: buildHiredAgentDescription(p),
|
|
@@ -198,10 +198,10 @@ export function buildAgentMap(opts = {}) {
|
|
|
198
198
|
// 1.18.198 — NO `tools` allowlist. Researcher inherits every tool the
|
|
199
199
|
// parent has access to (Bash, Read, MCP wildcards, etc.). The earlier
|
|
200
200
|
// hardcoded ['Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'] blocked
|
|
201
|
-
// researcher from using the parent's MCP servers
|
|
202
|
-
// "Parallel SEO enrichment for 13 domains"
|
|
201
|
+
// researcher from using the parent's MCP servers. A delegated
|
|
202
|
+
// "Parallel SEO enrichment for 13 domains" subagent couldn't call
|
|
203
203
|
// `mcp__dataforseo__*` because it wasn't in the allowlist. Result: the
|
|
204
|
-
// subagent said "I can't do that" and
|
|
204
|
+
// subagent said "I can't do that" and the parent fell back to running 25
|
|
205
205
|
// sequential MCP calls in his own turn, defeating the fan-out.
|
|
206
206
|
//
|
|
207
207
|
// Read-only behavior is enforced in RESEARCHER_PROMPT (behavior class:
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* ## Owner approval signals (recent)
|
|
14
14
|
* APPROVED (do more like this):
|
|
15
15
|
* - cron/insight-check: "Apply lean mode to reduce prompt size"
|
|
16
|
-
* - agent/
|
|
16
|
+
* - agent/marketing-agent: "Add explicit citation requirement to system prompt"
|
|
17
17
|
*
|
|
18
18
|
* DENIED (avoid these patterns):
|
|
19
19
|
* - workflow/email-gen: "Replace template with LLM generation" ← user note: "too generic; loses voice"
|
|
@@ -31,7 +31,7 @@ export interface ApprovalSignal {
|
|
|
31
31
|
experimentId: string;
|
|
32
32
|
/** The area the proposal targeted (cron, agent, skill, soul, etc.). */
|
|
33
33
|
area: string;
|
|
34
|
-
/** The specific target (e.g., "insight-check", "
|
|
34
|
+
/** The specific target (e.g., "insight-check", "marketing-agent"). */
|
|
35
35
|
target: string;
|
|
36
36
|
/** The proposal's one-sentence hypothesis (truncated to 200 chars). */
|
|
37
37
|
hypothesis: string;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* ## Owner approval signals (recent)
|
|
14
14
|
* APPROVED (do more like this):
|
|
15
15
|
* - cron/insight-check: "Apply lean mode to reduce prompt size"
|
|
16
|
-
* - agent/
|
|
16
|
+
* - agent/marketing-agent: "Add explicit citation requirement to system prompt"
|
|
17
17
|
*
|
|
18
18
|
* DENIED (avoid these patterns):
|
|
19
19
|
* - workflow/email-gen: "Replace template with LLM generation" ← user note: "too generic; loses voice"
|
package/dist/agent/assistant.js
CHANGED
|
@@ -262,6 +262,9 @@ const TOOLS_SERVER = `${ASSISTANT_NAME.toLowerCase()}-tools`;
|
|
|
262
262
|
function mcpTool(name) {
|
|
263
263
|
return `mcp__${TOOLS_SERVER}__${name}`;
|
|
264
264
|
}
|
|
265
|
+
function mcpServerWildcard(serverName) {
|
|
266
|
+
return `mcp__${serverName}__*`;
|
|
267
|
+
}
|
|
265
268
|
const CLEMENTINE_CORE_TOOL_NAMES = [
|
|
266
269
|
'working_memory',
|
|
267
270
|
'user_model',
|
|
@@ -1748,6 +1751,31 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1748
1751
|
fullSurface: false,
|
|
1749
1752
|
};
|
|
1750
1753
|
}
|
|
1754
|
+
const profileComposioAllowList = profile?.allowedComposioToolkits;
|
|
1755
|
+
if (!toolsDisabledForCall
|
|
1756
|
+
&& !isPlanStep
|
|
1757
|
+
&& !toolRoute.fullSurface
|
|
1758
|
+
&& Array.isArray(toolRoute.composioToolkits)
|
|
1759
|
+
&& !Array.isArray(profileComposioAllowList)) {
|
|
1760
|
+
try {
|
|
1761
|
+
const { listConnectedToolkits, matchConnectedToolkitsInText } = await import('../integrations/composio/client.js');
|
|
1762
|
+
const composioRoutingText = [
|
|
1763
|
+
directScopeText,
|
|
1764
|
+
allowContextToolRoute ? contextRoutingText : '',
|
|
1765
|
+
].filter(Boolean).join('\n');
|
|
1766
|
+
const mentioned = matchConnectedToolkitsInText(composioRoutingText, await listConnectedToolkits());
|
|
1767
|
+
if (mentioned.length > 0) {
|
|
1768
|
+
toolRoute = {
|
|
1769
|
+
...toolRoute,
|
|
1770
|
+
composioToolkits: [...new Set([...toolRoute.composioToolkits, ...mentioned])],
|
|
1771
|
+
reason: 'matched',
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
catch (err) {
|
|
1776
|
+
logger.debug({ err }, 'Connected Composio toolkit text match failed (non-fatal)');
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1751
1779
|
let allowedTools = [];
|
|
1752
1780
|
const addAllowed = (...tools) => {
|
|
1753
1781
|
for (const tool of tools) {
|
|
@@ -1944,9 +1972,8 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1944
1972
|
if (!toolsDisabledForCall && !isPlanStep) {
|
|
1945
1973
|
try {
|
|
1946
1974
|
const { buildComposioMcpServers } = await import('../integrations/composio/mcp-bridge.js');
|
|
1947
|
-
const
|
|
1948
|
-
|
|
1949
|
-
? profileAllowList
|
|
1975
|
+
const allowList = Array.isArray(profileComposioAllowList)
|
|
1976
|
+
? profileComposioAllowList
|
|
1950
1977
|
: toolRoute.composioToolkits;
|
|
1951
1978
|
composioMcpServers = await buildComposioMcpServers(allowList);
|
|
1952
1979
|
}
|
|
@@ -1956,6 +1983,10 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1956
1983
|
}
|
|
1957
1984
|
}
|
|
1958
1985
|
const composioConnectedSlugs = Object.keys(composioMcpServers);
|
|
1986
|
+
if (!toolsDisabledForCall) {
|
|
1987
|
+
for (const slug of composioConnectedSlugs)
|
|
1988
|
+
addAllowed(mcpServerWildcard(slug));
|
|
1989
|
+
}
|
|
1959
1990
|
const { stable, volatile: volatilePromptPart } = this.buildSystemPrompt({
|
|
1960
1991
|
isHeartbeat, cronTier: isPlanStep ? null : cronTier, retrievalContext, profile, sessionKey, model: resolvedModel, verboseLevel, intentClassification,
|
|
1961
1992
|
contextTier: turnPolicy?.retrievalTier ?? (retrievalContext ? 'full' : 'core'),
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Why this exists (1.18.190)
|
|
6
6
|
* ──────────────────────────
|
|
7
|
-
* Before this, a complex multi-step user ask ("find the
|
|
7
|
+
* Before this, a complex multi-step user ask ("find the catalog project,
|
|
8
8
|
* build me an HTML report, deploy it to Netlify, verify the URL") got
|
|
9
9
|
* handed to a single monolithic bg-task worker. The worker had its own
|
|
10
10
|
* 200K context but still autocompact-thrashed because:
|
|
@@ -36,15 +36,14 @@
|
|
|
36
36
|
* a multi-domain ask into proper steps is not mechanical.
|
|
37
37
|
*
|
|
38
38
|
* If you're tempted to "save tokens" by flipping this to Haiku, read
|
|
39
|
-
*
|
|
40
|
-
* (~/.claude/plans/look-at-the-last-vivid-rossum.md). The whole point
|
|
39
|
+
* a recent root-cause plan first. The whole point
|
|
41
40
|
* of this ship is to NOT cut corners on the decomposition layer.
|
|
42
41
|
*/
|
|
43
42
|
import type { ProjectMeta } from './assistant.js';
|
|
44
43
|
export interface PlanStep {
|
|
45
44
|
/** 0-indexed position. */
|
|
46
45
|
index: number;
|
|
47
|
-
/** Short imperative title (e.g., "Find the
|
|
46
|
+
/** Short imperative title (e.g., "Find the catalog project"). */
|
|
48
47
|
title: string;
|
|
49
48
|
/** What this step does, in 1-2 sentences. The chained worker sees this. */
|
|
50
49
|
scope: string;
|
package/dist/agent/bg-planner.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Why this exists (1.18.190)
|
|
6
6
|
* ──────────────────────────
|
|
7
|
-
* Before this, a complex multi-step user ask ("find the
|
|
7
|
+
* Before this, a complex multi-step user ask ("find the catalog project,
|
|
8
8
|
* build me an HTML report, deploy it to Netlify, verify the URL") got
|
|
9
9
|
* handed to a single monolithic bg-task worker. The worker had its own
|
|
10
10
|
* 200K context but still autocompact-thrashed because:
|
|
@@ -36,8 +36,7 @@
|
|
|
36
36
|
* a multi-domain ask into proper steps is not mechanical.
|
|
37
37
|
*
|
|
38
38
|
* If you're tempted to "save tokens" by flipping this to Haiku, read
|
|
39
|
-
*
|
|
40
|
-
* (~/.claude/plans/look-at-the-last-vivid-rossum.md). The whole point
|
|
39
|
+
* a recent root-cause plan first. The whole point
|
|
41
40
|
* of this ship is to NOT cut corners on the decomposition layer.
|
|
42
41
|
*/
|
|
43
42
|
import fs from 'node:fs';
|
|
@@ -202,7 +201,7 @@ function buildPlannerSystemPrompt() {
|
|
|
202
201
|
'{',
|
|
203
202
|
' "steps": [',
|
|
204
203
|
' {',
|
|
205
|
-
' "title": "<short imperative title, e.g. \'Find the
|
|
204
|
+
' "title": "<short imperative title, e.g. \'Find the catalog project\'>",',
|
|
206
205
|
' "scope": "<1-2 sentences describing exactly what this step does>",',
|
|
207
206
|
' "expectedTools": ["tool_name_1", "tool_name_2"],',
|
|
208
207
|
' "deliverable": "<file path | URL | description of the artifact>"',
|
|
@@ -273,7 +272,7 @@ async function runPlannerLlm(userPrompt, systemPrompt, model) {
|
|
|
273
272
|
// Raw `systemPrompt: string` tells the SDK to use API-key auth, which
|
|
274
273
|
// 99% of installs don't have configured — they're logged into Claude
|
|
275
274
|
// Code, not the Anthropic API. This was the "Not logged in · Please
|
|
276
|
-
// run /login" failure
|
|
275
|
+
// run /login" provider-auth failure.
|
|
277
276
|
//
|
|
278
277
|
// The preset injects Claude Code's default system prompt; our planning
|
|
279
278
|
// instructions go in `append` and dominate behavior for the single
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Why this exists (1.18.187)
|
|
6
6
|
* ──────────────────────────
|
|
7
7
|
* On 2026-05-11 a bg task was diagnosed where Clementine said "The
|
|
8
|
-
* site is live again at https://X.netlify.app — all 100
|
|
8
|
+
* site is live again at https://X.netlify.app — all 100 product records with
|
|
9
9
|
* search/filter/sort intact" — but the live URL returned HTTP 404,
|
|
10
10
|
* and the run had zero tool calls matching a deploy. She had
|
|
11
11
|
* confabulated success from a recall summary of a PRIOR task.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Why this exists (1.18.187)
|
|
6
6
|
* ──────────────────────────
|
|
7
7
|
* On 2026-05-11 a bg task was diagnosed where Clementine said "The
|
|
8
|
-
* site is live again at https://X.netlify.app — all 100
|
|
8
|
+
* site is live again at https://X.netlify.app — all 100 product records with
|
|
9
9
|
* search/filter/sort intact" — but the live URL returned HTTP 404,
|
|
10
10
|
* and the run had zero tool calls matching a deploy. She had
|
|
11
11
|
* confabulated success from a recall summary of a PRIOR task.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { RunSummary, SideEffectCall } from './run-summary.js';
|
|
2
|
+
export type PendingAgentDecisionKind = 'blocked_external_action';
|
|
3
|
+
export type BlockedActionCategory = 'deployment_target_missing';
|
|
4
|
+
export interface PendingAgentDecision {
|
|
5
|
+
id: string;
|
|
6
|
+
kind: PendingAgentDecisionKind;
|
|
7
|
+
createdAt: number;
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
runIds: string[];
|
|
10
|
+
originalRequest: string;
|
|
11
|
+
question: string;
|
|
12
|
+
context: {
|
|
13
|
+
category: BlockedActionCategory;
|
|
14
|
+
classifierId: string;
|
|
15
|
+
provider: string;
|
|
16
|
+
providerLabel: string;
|
|
17
|
+
blockerSummary: string;
|
|
18
|
+
failedCommand: string;
|
|
19
|
+
error: string;
|
|
20
|
+
targetNoun: string;
|
|
21
|
+
targetPlaceholder: string;
|
|
22
|
+
createInstructions: string[];
|
|
23
|
+
existingInstructions: string[];
|
|
24
|
+
projectPath?: string;
|
|
25
|
+
agentId?: string;
|
|
26
|
+
completedSideEffects?: string[];
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export type AgentDecisionReply = {
|
|
30
|
+
kind: 'answer';
|
|
31
|
+
action: 'create_new_target';
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'answer';
|
|
34
|
+
action: 'use_existing_target';
|
|
35
|
+
target: string;
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'cancel';
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'unclear';
|
|
40
|
+
message: string;
|
|
41
|
+
};
|
|
42
|
+
export interface BlockedActionClassifier {
|
|
43
|
+
id: string;
|
|
44
|
+
category: BlockedActionCategory;
|
|
45
|
+
provider: string;
|
|
46
|
+
providerLabel: string;
|
|
47
|
+
targetNoun: string;
|
|
48
|
+
targetPlaceholder: string;
|
|
49
|
+
defaultCommand: string;
|
|
50
|
+
defaultError: string;
|
|
51
|
+
blockerSummary: string;
|
|
52
|
+
matches(call: SideEffectCall): boolean;
|
|
53
|
+
createInstructions: string[];
|
|
54
|
+
existingInstructions: string[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Extension point for install-specific tool/provider blockers. Core owns the
|
|
58
|
+
* state machine; providers own only small classifiers and resume instructions.
|
|
59
|
+
*/
|
|
60
|
+
export declare function registerBlockedActionClassifier(classifier: BlockedActionClassifier): () => void;
|
|
61
|
+
export declare function buildBlockedActionDecisionFromRunSummary(summary: RunSummary, originalRequest: string, nowMs?: number): PendingAgentDecision | null;
|
|
62
|
+
export declare function parseAgentDecisionReply(decision: PendingAgentDecision, message: string): AgentDecisionReply;
|
|
63
|
+
export declare function buildAgentDecisionContinuationPrompt(decision: PendingAgentDecision, reply: Extract<AgentDecisionReply, {
|
|
64
|
+
kind: 'answer';
|
|
65
|
+
}>): string;
|
|
66
|
+
export declare const buildRepairDecisionFromRunSummary: typeof buildBlockedActionDecisionFromRunSummary;
|
|
67
|
+
//# sourceMappingURL=clarification-gate.d.ts.map
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
const customClassifiers = [];
|
|
2
|
+
/**
|
|
3
|
+
* Extension point for install-specific tool/provider blockers. Core owns the
|
|
4
|
+
* state machine; providers own only small classifiers and resume instructions.
|
|
5
|
+
*/
|
|
6
|
+
export function registerBlockedActionClassifier(classifier) {
|
|
7
|
+
customClassifiers.unshift(classifier);
|
|
8
|
+
return () => {
|
|
9
|
+
const index = customClassifiers.indexOf(classifier);
|
|
10
|
+
if (index >= 0)
|
|
11
|
+
customClassifiers.splice(index, 1);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function firstString(...values) {
|
|
15
|
+
for (const value of values) {
|
|
16
|
+
if (typeof value === 'string' && value.trim())
|
|
17
|
+
return value.trim();
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
function extractBashCommand(call) {
|
|
22
|
+
return firstString(call.input.command);
|
|
23
|
+
}
|
|
24
|
+
function sideEffectErrorText(call) {
|
|
25
|
+
return [
|
|
26
|
+
call.result?.error,
|
|
27
|
+
typeof call.result?.raw === 'string' ? call.result.raw : '',
|
|
28
|
+
].filter(Boolean).join('\n');
|
|
29
|
+
}
|
|
30
|
+
function compactCommand(command) {
|
|
31
|
+
return command.replace(/\s+/g, ' ').trim().slice(0, 260);
|
|
32
|
+
}
|
|
33
|
+
function compactValue(value, max = 220) {
|
|
34
|
+
const compacted = value.replace(/\s+/g, ' ').trim();
|
|
35
|
+
return compacted.length > max ? `${compacted.slice(0, max - 3)}...` : compacted;
|
|
36
|
+
}
|
|
37
|
+
function extractProjectPathFromCommand(command) {
|
|
38
|
+
return command.match(/\bcd\s+"([^"]+)"/)?.[1]
|
|
39
|
+
?? command.match(/\bcd\s+'([^']+)'/)?.[1]
|
|
40
|
+
?? command.match(/\bcd\s+([^&;|]+)/)?.[1]?.trim();
|
|
41
|
+
}
|
|
42
|
+
function extractFilePathFromCall(call) {
|
|
43
|
+
const fromInput = firstString(call.input.file_path, call.input.filePath, call.input.path, call.input.target_path, call.input.targetPath);
|
|
44
|
+
if (fromInput)
|
|
45
|
+
return fromInput;
|
|
46
|
+
const raw = call.result?.raw;
|
|
47
|
+
if (typeof raw === 'string') {
|
|
48
|
+
return raw.match(/\b(?:File (?:created|updated) successfully at|file state is current in your context):\s*([^\n(]+)/i)?.[1]?.trim();
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
function summarizeCompletedSideEffect(call) {
|
|
53
|
+
if (call.toolName === 'Bash') {
|
|
54
|
+
const command = extractBashCommand(call);
|
|
55
|
+
return command ? `Bash command completed: ${compactCommand(command)}` : 'Bash command completed';
|
|
56
|
+
}
|
|
57
|
+
const filePath = extractFilePathFromCall(call);
|
|
58
|
+
if (filePath)
|
|
59
|
+
return `${call.toolName} completed for file: ${filePath}`;
|
|
60
|
+
return `${call.toolName} completed`;
|
|
61
|
+
}
|
|
62
|
+
function collectText(value) {
|
|
63
|
+
if (typeof value === 'string')
|
|
64
|
+
return value;
|
|
65
|
+
if (Array.isArray(value))
|
|
66
|
+
return value.map(collectText).filter(Boolean).join('\n');
|
|
67
|
+
if (!value || typeof value !== 'object')
|
|
68
|
+
return '';
|
|
69
|
+
const obj = value;
|
|
70
|
+
return ['text', 'content', 'result', 'message']
|
|
71
|
+
.map((key) => collectText(obj[key]))
|
|
72
|
+
.filter(Boolean)
|
|
73
|
+
.join('\n');
|
|
74
|
+
}
|
|
75
|
+
function extractAgentId(summary) {
|
|
76
|
+
for (const call of summary.successfulDelegations) {
|
|
77
|
+
const text = call.result ? collectText(call.result.raw) : '';
|
|
78
|
+
const match = text.match(/\bagentId:\s*([a-zA-Z0-9_-]+)/);
|
|
79
|
+
if (match?.[1])
|
|
80
|
+
return match[1];
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const BUILTIN_CLASSIFIERS = [
|
|
85
|
+
{
|
|
86
|
+
id: 'netlify_missing_deployment_target',
|
|
87
|
+
category: 'deployment_target_missing',
|
|
88
|
+
provider: 'netlify',
|
|
89
|
+
providerLabel: 'Netlify',
|
|
90
|
+
targetNoun: 'deployment target',
|
|
91
|
+
targetPlaceholder: 'target-slug-or-id',
|
|
92
|
+
defaultCommand: 'netlify deploy',
|
|
93
|
+
defaultError: 'Deployment target is not linked',
|
|
94
|
+
blockerSummary: 'The deployment provider reported that this project is not linked to a deployment target.',
|
|
95
|
+
matches(call) {
|
|
96
|
+
if (call.toolName !== 'Bash')
|
|
97
|
+
return false;
|
|
98
|
+
const command = extractBashCommand(call) ?? '';
|
|
99
|
+
const error = sideEffectErrorText(call);
|
|
100
|
+
return /\bnetlify\s+deploy\b/i.test(command)
|
|
101
|
+
&& /\bProject not found\. Please rerun "netlify link"|\bnetlify link\b/i.test(error);
|
|
102
|
+
},
|
|
103
|
+
createInstructions: [
|
|
104
|
+
'Create or link a new deployment target for this project, then deploy and verify the live URL.',
|
|
105
|
+
'Do not restart project discovery or reread full generated artifacts unless a small targeted read is necessary.',
|
|
106
|
+
'If provider auth, browser login, or an interactive naming choice is required and cannot be completed safely, stop and ask one concrete question.',
|
|
107
|
+
'If a Clementine deploy config is appropriate, write `.clementine/deploy.json` with the provider kind, target identifier, deploy directory, and verify URL.',
|
|
108
|
+
'Prefer `project_deploy` once deploy config exists; otherwise run the equivalent provider deploy command and verify the live URL before claiming success.',
|
|
109
|
+
],
|
|
110
|
+
existingInstructions: [
|
|
111
|
+
'Use or link the existing deployment target: {target}',
|
|
112
|
+
'Do not restart project discovery or reread full generated artifacts unless a small targeted read is necessary.',
|
|
113
|
+
'Write or update `.clementine/deploy.json` for the existing target before deploying when that config is supported.',
|
|
114
|
+
'Prefer `project_deploy` once deploy config exists; otherwise run the equivalent provider deploy command and verify the live URL before claiming success.',
|
|
115
|
+
'If the provider rejects the target or auth is missing, stop and ask one concrete question with the exact CLI/API error.',
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
function blockedActionClassifiers() {
|
|
120
|
+
return [...customClassifiers, ...BUILTIN_CLASSIFIERS];
|
|
121
|
+
}
|
|
122
|
+
function makeDecisionId(kind) {
|
|
123
|
+
return `${kind}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
124
|
+
}
|
|
125
|
+
function formatDecisionPrompt(decision) {
|
|
126
|
+
return [
|
|
127
|
+
'I need one decision before I can continue this external action.',
|
|
128
|
+
'',
|
|
129
|
+
decision.context.blockerSummary,
|
|
130
|
+
`- Provider: ${decision.context.providerLabel}`,
|
|
131
|
+
`- Failed command: \`${decision.context.failedCommand}\``,
|
|
132
|
+
`- Provider said: ${decision.context.error}`,
|
|
133
|
+
...(decision.context.projectPath ? [`- Project folder: \`${decision.context.projectPath}\``] : []),
|
|
134
|
+
...(decision.context.agentId ? [`- Discovery already completed: agentId \`${decision.context.agentId}\``] : []),
|
|
135
|
+
'',
|
|
136
|
+
'Reply with one of these:',
|
|
137
|
+
`- \`create target\` to create/link a new ${decision.context.targetNoun}, deploy, and verify the result.`,
|
|
138
|
+
`- \`use existing <${decision.context.targetPlaceholder}>\` to use a target you already have.`,
|
|
139
|
+
'- `done` to stop here.',
|
|
140
|
+
].join('\n');
|
|
141
|
+
}
|
|
142
|
+
export function buildBlockedActionDecisionFromRunSummary(summary, originalRequest, nowMs = Date.now()) {
|
|
143
|
+
for (const failedCall of summary.failedSideEffects) {
|
|
144
|
+
const classifier = blockedActionClassifiers().find(c => c.matches(failedCall));
|
|
145
|
+
if (!classifier)
|
|
146
|
+
continue;
|
|
147
|
+
const rawCommand = extractBashCommand(failedCall) ?? classifier.defaultCommand;
|
|
148
|
+
const failedCommand = compactCommand(rawCommand);
|
|
149
|
+
const error = sideEffectErrorText(failedCall).trim() || classifier.defaultError;
|
|
150
|
+
const projectPath = extractProjectPathFromCommand(rawCommand);
|
|
151
|
+
const agentId = extractAgentId(summary);
|
|
152
|
+
const completedSideEffects = summary.successfulSideEffects
|
|
153
|
+
.map(summarizeCompletedSideEffect)
|
|
154
|
+
.map((line) => compactValue(line))
|
|
155
|
+
.slice(0, 5);
|
|
156
|
+
const decision = {
|
|
157
|
+
id: makeDecisionId('blocked_external_action'),
|
|
158
|
+
kind: 'blocked_external_action',
|
|
159
|
+
createdAt: nowMs,
|
|
160
|
+
expiresAt: nowMs + 30 * 60_000,
|
|
161
|
+
runIds: summary.runIds,
|
|
162
|
+
originalRequest,
|
|
163
|
+
question: '',
|
|
164
|
+
context: {
|
|
165
|
+
category: classifier.category,
|
|
166
|
+
classifierId: classifier.id,
|
|
167
|
+
provider: classifier.provider,
|
|
168
|
+
providerLabel: classifier.providerLabel,
|
|
169
|
+
blockerSummary: classifier.blockerSummary,
|
|
170
|
+
failedCommand,
|
|
171
|
+
error: compactValue(error, 500),
|
|
172
|
+
targetNoun: classifier.targetNoun,
|
|
173
|
+
targetPlaceholder: classifier.targetPlaceholder,
|
|
174
|
+
createInstructions: classifier.createInstructions,
|
|
175
|
+
existingInstructions: classifier.existingInstructions,
|
|
176
|
+
...(projectPath ? { projectPath } : {}),
|
|
177
|
+
...(agentId ? { agentId } : {}),
|
|
178
|
+
...(completedSideEffects.length > 0 ? { completedSideEffects } : {}),
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
decision.question = formatDecisionPrompt(decision);
|
|
182
|
+
return decision;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
function unclearDecisionMessage(decision) {
|
|
187
|
+
return `I need a specific decision: reply \`create target\`, \`use existing <${decision.context.targetPlaceholder}>\`, or \`done\`.`;
|
|
188
|
+
}
|
|
189
|
+
export function parseAgentDecisionReply(decision, message) {
|
|
190
|
+
const text = message.trim();
|
|
191
|
+
const lower = text.toLowerCase().replace(/[.!?]+$/g, '').replace(/\s+/g, ' ').trim();
|
|
192
|
+
const intent = lower
|
|
193
|
+
.replace(/^(?:please|yes|yep|yeah|sure|ok|okay|go ahead(?: and)?)\s+/, '')
|
|
194
|
+
.replace(/\s+please$/, '')
|
|
195
|
+
.trim();
|
|
196
|
+
if (!lower) {
|
|
197
|
+
return {
|
|
198
|
+
kind: 'unclear',
|
|
199
|
+
message: unclearDecisionMessage(decision),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (/^(?:done|stop|cancel|abort|no|nope|that's all|that is all|leave it)\b/.test(lower)) {
|
|
203
|
+
return { kind: 'cancel' };
|
|
204
|
+
}
|
|
205
|
+
if (/^(?:create|make|new)(?:\s+(?:(?:a|the)\s+)?(?:new\s+)?(?:target|deployment target|site|project|one))?$/.test(intent)
|
|
206
|
+
|| /^(?:create|make)\s+(?:it|one)$/.test(intent)
|
|
207
|
+
|| /\b(?:create|make)\s+(?:(?:a|the)\s+)?(?:new\s+)?(?:deployment\s+)?(?:target|site|project)\b/.test(intent)
|
|
208
|
+
|| /\bnew\s+(?:deployment\s+)?(?:target|site|project)\b/.test(intent)) {
|
|
209
|
+
return { kind: 'answer', action: 'create_new_target' };
|
|
210
|
+
}
|
|
211
|
+
const explicitTarget = text.match(/^\s*(?:use|link)\s+(?:to\s+)?(?:the\s+)?(?:existing\s+)?(?:deployment\s+)?(?:target|site|project)\s+(.+?)\s*$/i)
|
|
212
|
+
?? text.match(/^\s*(?:use|link)\s+(?:to\s+)?(?:existing\s+)?(.+?)\s*$/i)
|
|
213
|
+
?? text.match(/^\s*target\s*:\s*(.+?)\s*$/i);
|
|
214
|
+
if (explicitTarget?.[1]) {
|
|
215
|
+
const target = explicitTarget[1].trim().replace(/^["'`]|["'`]$/g, '');
|
|
216
|
+
if (/[<>]/.test(target)) {
|
|
217
|
+
return {
|
|
218
|
+
kind: 'unclear',
|
|
219
|
+
message: `Please replace \`<${decision.context.targetPlaceholder}>\` with the actual ${decision.context.targetNoun} identifier or URL.`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (target)
|
|
223
|
+
return { kind: 'answer', action: 'use_existing_target', target };
|
|
224
|
+
}
|
|
225
|
+
const url = text.match(/https?:\/\/\S+/i);
|
|
226
|
+
if (url?.[0]) {
|
|
227
|
+
return { kind: 'answer', action: 'use_existing_target', target: url[0].replace(/[),.]+$/g, '') };
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
kind: 'unclear',
|
|
231
|
+
message: unclearDecisionMessage(decision),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function renderInstructions(instructions, target) {
|
|
235
|
+
return instructions.map(line => target ? line.replace(/\{target\}/g, target) : line);
|
|
236
|
+
}
|
|
237
|
+
export function buildAgentDecisionContinuationPrompt(decision, reply) {
|
|
238
|
+
const lines = [
|
|
239
|
+
'[Agentic repair loop - read this before taking any action]',
|
|
240
|
+
'State transition: needs_user_decision -> executing',
|
|
241
|
+
`Decision kind: ${decision.kind}`,
|
|
242
|
+
`Blocker category: ${decision.context.category}`,
|
|
243
|
+
`Provider: ${decision.context.providerLabel}`,
|
|
244
|
+
`Previous run(s): ${decision.runIds.join(', ')}`,
|
|
245
|
+
'',
|
|
246
|
+
'Original owner request:',
|
|
247
|
+
decision.originalRequest,
|
|
248
|
+
'',
|
|
249
|
+
'Blocked step:',
|
|
250
|
+
`- Failed command: ${decision.context.failedCommand}`,
|
|
251
|
+
`- Error: ${decision.context.error}`,
|
|
252
|
+
...(decision.context.projectPath ? [`- Project folder already identified: ${decision.context.projectPath}`] : []),
|
|
253
|
+
...(decision.context.agentId ? [`- Discovery already completed by agentId ${decision.context.agentId}`] : []),
|
|
254
|
+
'',
|
|
255
|
+
];
|
|
256
|
+
if (decision.context.completedSideEffects?.length) {
|
|
257
|
+
lines.push('Completed before the block. Do not repeat these unless verification proves they are stale:', ...decision.context.completedSideEffects.map((line) => `- ${line}`), '');
|
|
258
|
+
}
|
|
259
|
+
if (reply.action === 'create_new_target') {
|
|
260
|
+
lines.push('Owner decision:', `- Create/link a new ${decision.context.targetNoun}, then deploy and verify.`, '', 'Execution requirements:', ...renderInstructions(decision.context.createInstructions).map(line => `- ${line}`));
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
lines.push('Owner decision:', `- Use existing ${decision.context.targetNoun}: ${reply.target}`, '', 'Execution requirements:', ...renderInstructions(decision.context.existingInstructions, reply.target).map(line => `- ${line}`));
|
|
264
|
+
}
|
|
265
|
+
lines.push('[/Agentic repair loop]');
|
|
266
|
+
return lines.join('\n');
|
|
267
|
+
}
|
|
268
|
+
// Backward-compatible alias for the router/tests while callers migrate to the
|
|
269
|
+
// provider-neutral name.
|
|
270
|
+
export const buildRepairDecisionFromRunSummary = buildBlockedActionDecisionFromRunSummary;
|
|
271
|
+
//# sourceMappingURL=clarification-gate.js.map
|
|
@@ -57,7 +57,7 @@ export interface BuildTurnContextOptions {
|
|
|
57
57
|
* the identity framing block. */
|
|
58
58
|
ownerName?: string | null;
|
|
59
59
|
/** Active hired-agent profile if running as one. Affects the
|
|
60
|
-
* identity framing — "you are talking to
|
|
60
|
+
* identity framing — "you are talking to this hired agent right now," not
|
|
61
61
|
* "you are Clementine". */
|
|
62
62
|
profileName?: string | null;
|
|
63
63
|
/** Read-only memory store handle. When absent, retrieved-memory
|
|
@@ -326,10 +326,14 @@ function buildActiveProjectBlock(project) {
|
|
|
326
326
|
if (parsed && typeof parsed === 'object') {
|
|
327
327
|
const kind = parsed.kind ?? 'unknown';
|
|
328
328
|
const site = parsed.site ?? '?';
|
|
329
|
-
const
|
|
329
|
+
const command = typeof parsed.command === 'string' ? parsed.command.replace(/\s+/g, ' ').trim() : '';
|
|
330
|
+
const dir = parsed.dir ?? (kind === 'netlify' ? 'output' : '?');
|
|
330
331
|
const verifyUrl = parsed.verifyUrl ?? '?';
|
|
331
332
|
lines.push('');
|
|
332
|
-
|
|
333
|
+
const target = command
|
|
334
|
+
? `command \`${command.slice(0, 160)}${command.length > 160 ? '...' : ''}\``
|
|
335
|
+
: `target ${site}`;
|
|
336
|
+
lines.push(`**Deploy config:** ${kind} -> ${target} (deploy dir: \`${dir}\`, verifies at ${verifyUrl}).`);
|
|
333
337
|
lines.push('Use the \`project_deploy\` tool when ready — it runs the command AND curls the URL to verify before reporting success.');
|
|
334
338
|
}
|
|
335
339
|
}
|
|
@@ -338,7 +342,8 @@ function buildActiveProjectBlock(project) {
|
|
|
338
342
|
else {
|
|
339
343
|
lines.push('');
|
|
340
344
|
lines.push('**No deploy config yet.** If this project should deploy somewhere, ask the owner where and ' +
|
|
341
|
-
'write `.clementine/deploy.json` with shape
|
|
345
|
+
'write `.clementine/deploy.json` with shape like `{kind: "custom", command: "npm run deploy", verifyUrl: "https://..."}`. ' +
|
|
346
|
+
'Provider adapters can add target fields such as `site` and `dir`.');
|
|
342
347
|
}
|
|
343
348
|
return lines.join('\n');
|
|
344
349
|
}
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Durable-work detector.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* gateway's overflow → retry → promote-to-background fallback, which is the
|
|
11
|
-
* *real* escape hatch instead of a regex pre-classifier.
|
|
12
|
-
*
|
|
13
|
-
* The narrow detection here is what lets a user say "go research this
|
|
14
|
-
* overnight" and have it actually queue as a durable background task.
|
|
4
|
+
* Chat should stay live for normal multi-step work, but truly batch-heavy
|
|
5
|
+
* jobs should not require the owner to know the magic phrase "run this in the
|
|
6
|
+
* background." If the request clearly implies a long first pass — e.g. 100
|
|
7
|
+
* businesses, multiple external systems, research/enrichment plus sheet/email
|
|
8
|
+
* side effects — queue a durable background task immediately. Small project
|
|
9
|
+
* builds and single targeted actions still run in chat.
|
|
15
10
|
*/
|
|
16
11
|
export interface ComplexTaskRecommendation {
|
|
17
12
|
reasons: string[];
|