clementine-agent 1.18.209 → 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.
Files changed (52) hide show
  1. package/dist/agent/agent-definitions.js +10 -10
  2. package/dist/agent/approval-signals.d.ts +2 -2
  3. package/dist/agent/approval-signals.js +1 -1
  4. package/dist/agent/assistant.js +34 -3
  5. package/dist/agent/bg-planner.d.ts +3 -4
  6. package/dist/agent/bg-planner.js +4 -5
  7. package/dist/agent/claim-verification.d.ts +1 -1
  8. package/dist/agent/claim-verification.js +1 -1
  9. package/dist/agent/clarification-gate.d.ts +67 -0
  10. package/dist/agent/clarification-gate.js +271 -0
  11. package/dist/agent/clementine-turn-context.d.ts +1 -1
  12. package/dist/agent/clementine-turn-context.js +8 -3
  13. package/dist/agent/complex-task-detector.d.ts +7 -12
  14. package/dist/agent/complex-task-detector.js +70 -26
  15. package/dist/agent/execution-policy.d.ts +23 -0
  16. package/dist/agent/execution-policy.js +36 -0
  17. package/dist/agent/intent-classifier.d.ts +1 -1
  18. package/dist/agent/project-resolver.d.ts +3 -3
  19. package/dist/agent/project-resolver.js +7 -7
  20. package/dist/agent/role-scaffolds.d.ts +1 -1
  21. package/dist/agent/role-scaffolds.js +1 -1
  22. package/dist/agent/run-agent-context.js +4 -5
  23. package/dist/agent/run-agent-cron.d.ts +2 -3
  24. package/dist/agent/run-agent-cron.js +7 -3
  25. package/dist/agent/run-agent-heartbeat.js +6 -2
  26. package/dist/agent/run-agent-mcp.d.ts +1 -2
  27. package/dist/agent/run-agent-mcp.js +29 -5
  28. package/dist/agent/run-agent-team-task.js +6 -3
  29. package/dist/agent/run-agent.js +2 -2
  30. package/dist/agent/schedule-registry.d.ts +2 -2
  31. package/dist/agent/skill-suppressions.d.ts +1 -1
  32. package/dist/agent/skill-suppressions.js +1 -1
  33. package/dist/agent/turn-policy.js +3 -3
  34. package/dist/channels/discord-agent-bot.js +2 -12
  35. package/dist/channels/discord-utils.d.ts +2 -0
  36. package/dist/channels/discord-utils.js +32 -0
  37. package/dist/channels/discord.js +2 -12
  38. package/dist/cli/dashboard.js +7 -6
  39. package/dist/config.js +1 -1
  40. package/dist/gateway/failure-monitor.js +9 -2
  41. package/dist/gateway/heartbeat-scheduler.js +1 -1
  42. package/dist/gateway/router.d.ts +1 -0
  43. package/dist/gateway/router.js +101 -30
  44. package/dist/integrations/composio/client.d.ts +6 -0
  45. package/dist/integrations/composio/client.js +117 -10
  46. package/dist/memory/store.js +2 -2
  47. package/dist/tools/admin-tools.js +2 -2
  48. package/dist/tools/project-tools.d.ts +1 -1
  49. package/dist/tools/project-tools.js +68 -17
  50. package/dist/tools/schedule-tools.js +1 -1
  51. package/dist/vault-migrations/0002-add-agentic-communication.js +1 -1
  52. 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
- // Always include `Agent` so the subagent can further fan out, plus
148
- // core read tools as a baseline. profile.team.allowedTools narrows
149
- // beyond this when set.
150
- const baseline = ['Agent', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch', 'TodoWrite'];
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(['Agent', ...p.team.allowedTools]))
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 when Ross dispatched
202
- // "Parallel SEO enrichment for 13 domains" the subagent couldn't call
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 Ross fell back to running 25
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/sasha-the-cmo: "Add explicit citation requirement to system prompt"
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", "sasha-the-cmo"). */
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/sasha-the-cmo: "Add explicit citation requirement to system prompt"
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"
@@ -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 profileAllowList = profile?.allowedComposioToolkits;
1948
- const allowList = Array.isArray(profileAllowList)
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 coaches project,
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
- * the 2026-05-12 root-cause plan first
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 coaches project"). */
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;
@@ -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 coaches project,
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
- * the 2026-05-12 root-cause plan first
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 coaches project\'>",',
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 Ross's owner hit on 2026-05-12.
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 coaches with
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 coaches with
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 Sasha right now," not
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 dir = parsed.dir ?? 'output';
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
- lines.push(`**Deploy config:** ${kind} → ${site} (deploy dir: \`${dir}\`, verifies at ${verifyUrl}).`);
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: `{kind: "netlify", site: "...", dir: "output", verifyUrl: "..."}`.');
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
- * Explicit background-intent detector.
2
+ * Durable-work detector.
3
3
  *
4
- * Returns a recommendation ONLY when the user explicitly asks for background
5
- * / autonomous / overnight execution. We deliberately do not classify "this
6
- * looks complex" anymore chat now stays in the live SDK loop, with
7
- * automatic compaction and inline subagent delegation (Agent → planner /
8
- * researcher / etc.) for context isolation, just like Claude Code itself.
9
- * Big work that genuinely blows past the SDK's auto-compact is caught by the
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[];