clementine-agent 1.18.210 → 1.18.211

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.
@@ -58,10 +58,59 @@ export interface BlockedActionClassifier {
58
58
  * state machine; providers own only small classifiers and resume instructions.
59
59
  */
60
60
  export declare function registerBlockedActionClassifier(classifier: BlockedActionClassifier): () => void;
61
+ export interface PreconditionEnv {
62
+ /** Working directory the agent will use for this tool call. */
63
+ cwd: string;
64
+ /** Active project path if the router resolved one. */
65
+ activeProjectPath?: string;
66
+ /** filesystem.existsSync injection point for testability. */
67
+ existsSync?: (path: string) => boolean;
68
+ }
69
+ export interface PreconditionMatch {
70
+ /** The command or tool action that would have run (for owner display). */
71
+ attemptedCommand: string;
72
+ /** The reason the precondition failed (for owner display). */
73
+ reason: string;
74
+ /** Optional project path inferred from the input (e.g. `cd /path && netlify deploy`). */
75
+ projectPath?: string;
76
+ }
77
+ export interface PreconditionClassifier {
78
+ id: string;
79
+ category: BlockedActionCategory;
80
+ provider: string;
81
+ providerLabel: string;
82
+ targetNoun: string;
83
+ targetPlaceholder: string;
84
+ blockerSummary: string;
85
+ /** Tool names this classifier inspects. e.g. ['Bash'] or
86
+ * ['mcp__clementine-tools__project_deploy']. Wildcard `'*'` not supported —
87
+ * classifiers should be narrow on purpose. */
88
+ toolNames: readonly string[];
89
+ /** Returns a PreconditionMatch when the call should be blocked, or null
90
+ * to let it proceed. Errors thrown here are caught and treated as a pass
91
+ * (fail-open) — the post-hoc classifier still catches the failure. */
92
+ matchesPreconditions(input: Record<string, unknown>, env: PreconditionEnv): PreconditionMatch | null;
93
+ createInstructions: string[];
94
+ existingInstructions: string[];
95
+ }
96
+ /** Extension point for pre-call classifiers. Mirrors registerBlockedActionClassifier. */
97
+ export declare function registerPreconditionClassifier(classifier: PreconditionClassifier): () => void;
61
98
  export declare function buildBlockedActionDecisionFromRunSummary(summary: RunSummary, originalRequest: string, nowMs?: number): PendingAgentDecision | null;
62
99
  export declare function parseAgentDecisionReply(decision: PendingAgentDecision, message: string): AgentDecisionReply;
63
100
  export declare function buildAgentDecisionContinuationPrompt(decision: PendingAgentDecision, reply: Extract<AgentDecisionReply, {
64
101
  kind: 'answer';
65
102
  }>): string;
66
103
  export declare const buildRepairDecisionFromRunSummary: typeof buildBlockedActionDecisionFromRunSummary;
104
+ /**
105
+ * Run all registered precondition classifiers against an attempted tool
106
+ * call. Returns the first PendingAgentDecision that matches, or null if
107
+ * the call should proceed. Errors inside individual classifiers are
108
+ * swallowed (fail-open) — the post-hoc classifier remains as the safety
109
+ * net for any case the pre-call rule mishandles.
110
+ */
111
+ export declare function evaluatePreconditionsForToolCall(toolName: string, input: Record<string, unknown>, env: PreconditionEnv, opts?: {
112
+ originalRequest?: string;
113
+ runId?: string;
114
+ nowMs?: number;
115
+ }): PendingAgentDecision | null;
67
116
  //# sourceMappingURL=clarification-gate.d.ts.map
@@ -11,6 +11,16 @@ export function registerBlockedActionClassifier(classifier) {
11
11
  customClassifiers.splice(index, 1);
12
12
  };
13
13
  }
14
+ const customPreconditionClassifiers = [];
15
+ /** Extension point for pre-call classifiers. Mirrors registerBlockedActionClassifier. */
16
+ export function registerPreconditionClassifier(classifier) {
17
+ customPreconditionClassifiers.unshift(classifier);
18
+ return () => {
19
+ const index = customPreconditionClassifiers.indexOf(classifier);
20
+ if (index >= 0)
21
+ customPreconditionClassifiers.splice(index, 1);
22
+ };
23
+ }
14
24
  function firstString(...values) {
15
25
  for (const value of values) {
16
26
  if (typeof value === 'string' && value.trim())
@@ -268,4 +278,125 @@ export function buildAgentDecisionContinuationPrompt(decision, reply) {
268
278
  // Backward-compatible alias for the router/tests while callers migrate to the
269
279
  // provider-neutral name.
270
280
  export const buildRepairDecisionFromRunSummary = buildBlockedActionDecisionFromRunSummary;
281
+ // ─── Pre-call decision construction ────────────────────────────────────
282
+ function preconditionClassifiers() {
283
+ return [...customPreconditionClassifiers, ...BUILTIN_PRECONDITION_CLASSIFIERS];
284
+ }
285
+ /**
286
+ * Run all registered precondition classifiers against an attempted tool
287
+ * call. Returns the first PendingAgentDecision that matches, or null if
288
+ * the call should proceed. Errors inside individual classifiers are
289
+ * swallowed (fail-open) — the post-hoc classifier remains as the safety
290
+ * net for any case the pre-call rule mishandles.
291
+ */
292
+ export function evaluatePreconditionsForToolCall(toolName, input, env, opts = {}) {
293
+ const nowMs = opts.nowMs ?? Date.now();
294
+ for (const classifier of preconditionClassifiers()) {
295
+ if (!classifier.toolNames.includes(toolName))
296
+ continue;
297
+ let match = null;
298
+ try {
299
+ match = classifier.matchesPreconditions(input, env);
300
+ }
301
+ catch {
302
+ // Fail-open — the post-hoc classifier will catch a failure.
303
+ continue;
304
+ }
305
+ if (!match)
306
+ continue;
307
+ const decision = {
308
+ id: makeDecisionId('blocked_external_action'),
309
+ kind: 'blocked_external_action',
310
+ createdAt: nowMs,
311
+ expiresAt: nowMs + 30 * 60_000,
312
+ runIds: opts.runId ? [opts.runId] : [],
313
+ originalRequest: opts.originalRequest ?? '',
314
+ question: '',
315
+ context: {
316
+ category: classifier.category,
317
+ classifierId: classifier.id,
318
+ provider: classifier.provider,
319
+ providerLabel: classifier.providerLabel,
320
+ blockerSummary: classifier.blockerSummary,
321
+ failedCommand: compactCommand(match.attemptedCommand),
322
+ error: compactValue(match.reason, 500),
323
+ targetNoun: classifier.targetNoun,
324
+ targetPlaceholder: classifier.targetPlaceholder,
325
+ createInstructions: classifier.createInstructions,
326
+ existingInstructions: classifier.existingInstructions,
327
+ ...(match.projectPath ? { projectPath: match.projectPath } : {}),
328
+ },
329
+ };
330
+ decision.question = formatDecisionPrompt(decision);
331
+ return decision;
332
+ }
333
+ return null;
334
+ }
335
+ // ─── Built-in precondition classifiers ─────────────────────────────────
336
+ import { existsSync as nodeExistsSync } from 'node:fs';
337
+ import { join as pathJoin } from 'node:path';
338
+ /**
339
+ * Netlify deploy precondition: deny `netlify deploy` (or our project_deploy
340
+ * tool when its provider is netlify) when the project has no record of a
341
+ * linked site. Mirrors the post-hoc netlify_missing_deployment_target
342
+ * classifier but fires BEFORE the tool runs.
343
+ *
344
+ * Detection signals (any one is sufficient to allow):
345
+ * - `.netlify/state.json` exists in the project dir (netlify CLI's own
346
+ * link record)
347
+ * - `.clementine/deploy.json` exists in the project dir (Clementine's
348
+ * deploy config)
349
+ *
350
+ * If neither exists AND the project dir is identifiable, we deny pre-call
351
+ * and ask the owner. If the project dir is not identifiable (e.g. the
352
+ * agent invoked `netlify deploy` without a `cd` prefix), we fail-open
353
+ * and let the post-hoc classifier handle it.
354
+ */
355
+ const netlifyMissingLinkPrecondition = {
356
+ id: 'netlify_missing_deployment_target',
357
+ category: 'deployment_target_missing',
358
+ provider: 'netlify',
359
+ providerLabel: 'Netlify',
360
+ targetNoun: 'deployment target',
361
+ targetPlaceholder: 'target-slug-or-id',
362
+ blockerSummary: 'This project does not have a Netlify deployment target linked yet. Deploying will fail until one is configured.',
363
+ toolNames: ['Bash'],
364
+ matchesPreconditions(input, env) {
365
+ const command = firstString(input.command);
366
+ if (!command)
367
+ return null;
368
+ if (!/\bnetlify\s+deploy\b/i.test(command))
369
+ return null;
370
+ const projectPath = extractProjectPathFromCommand(command) ?? env.activeProjectPath;
371
+ if (!projectPath)
372
+ return null; // can't check — fail open
373
+ const exists = env.existsSync ?? nodeExistsSync;
374
+ const netlifyLinked = exists(pathJoin(projectPath, '.netlify', 'state.json'));
375
+ const clementineDeployConfig = exists(pathJoin(projectPath, '.clementine', 'deploy.json'));
376
+ if (netlifyLinked || clementineDeployConfig)
377
+ return null;
378
+ return {
379
+ attemptedCommand: compactCommand(command),
380
+ reason: 'No `.netlify/state.json` or `.clementine/deploy.json` found in the project. Netlify CLI will fail with "Project not found. Please rerun \'netlify link\'".',
381
+ projectPath,
382
+ };
383
+ },
384
+ createInstructions: [
385
+ 'Create or link a new Netlify deployment target for this project, then deploy and verify the live URL.',
386
+ 'Do not restart project discovery or reread full generated artifacts unless a small targeted read is necessary.',
387
+ 'If provider auth, browser login, or an interactive naming choice is required and cannot be completed safely, stop and ask one concrete question.',
388
+ 'If a Clementine deploy config is appropriate, write `.clementine/deploy.json` with the provider kind, target identifier, deploy directory, and verify URL.',
389
+ 'Prefer `project_deploy` once deploy config exists; otherwise run the equivalent provider deploy command and verify the live URL before claiming success.',
390
+ ],
391
+ existingInstructions: [
392
+ 'Use or link the existing Netlify target: {target}',
393
+ 'Do not restart project discovery or reread full generated artifacts unless a small targeted read is necessary.',
394
+ 'Write or update `.clementine/deploy.json` for the existing target before deploying when that config is supported.',
395
+ 'Prefer `project_deploy` once deploy config exists; otherwise run the equivalent provider deploy command and verify the live URL before claiming success.',
396
+ 'If the provider rejects the target or auth is missing, stop and ask one concrete question with the exact CLI/API error.',
397
+ ],
398
+ };
399
+ const BUILTIN_PRECONDITION_CLASSIFIERS = [
400
+ netlifyMissingLinkPrecondition,
401
+ ];
271
402
  //# sourceMappingURL=clarification-gate.js.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * precondition-guard — SDK PreToolUse hooks that evaluate the registered
3
+ * precondition classifiers before each tool call. When a classifier
4
+ * matches, the hook returns `permissionDecision: 'deny'` with a reason,
5
+ * captures the PendingAgentDecision in module-scoped state for the run,
6
+ * and runAgent surfaces it to the router via RunAgentResult.
7
+ *
8
+ * Why this exists
9
+ * ───────────────
10
+ * `clarification-gate.ts` already handles owner-decision routing for
11
+ * blocked external actions, but its existing BlockedActionClassifier API
12
+ * is post-hoc: the tool already ran and failed, so we parse the error,
13
+ * inferred the blocker, then asked the owner. That leaves partial state
14
+ * behind (a half-deploy, a created-but-unconfigured target, an emitted
15
+ * webhook that can't be undone).
16
+ *
17
+ * The orchestrator-first north star prefers pre-emptive gates. The SDK
18
+ * exposes PreToolUse hooks that run BEFORE every tool call regardless
19
+ * of permissionMode. We use them to short-circuit known-bad calls
20
+ * before they fire. The same PendingAgentDecision shape flows through
21
+ * the router, so the owner experience is identical — only the failure
22
+ * surface is narrower.
23
+ *
24
+ * Why PreToolUse over canUseTool
25
+ * ──────────────────────────────
26
+ * `canUseTool` is permission-prompt scoped and may be skipped under
27
+ * `bypassPermissions` mode. `PreToolUse` hooks fire universally. The
28
+ * SDK's own doc string on PermissionDeniedMessage confirms:
29
+ *
30
+ * "PreToolUse hook denies bypass canUseTool and are not covered here."
31
+ *
32
+ * That means PreToolUse decisions run FIRST, even when canUseTool is
33
+ * absent or short-circuited. PreToolUse is also already wired into the
34
+ * runAgent hooks pipeline (`tool-output-guard`, `dedup`,
35
+ * `idempotency`), so this fits the established pattern.
36
+ */
37
+ import type { HookCallbackMatcher, HookEvent } from '@anthropic-ai/claude-agent-sdk';
38
+ import { type PendingAgentDecision } from './clarification-gate.js';
39
+ export interface PreconditionGuardOptions {
40
+ /** Working directory the agent is running in. Used by classifiers to
41
+ * resolve relative project paths. */
42
+ cwd: string;
43
+ /** Active project path if the router resolved one. Optional. */
44
+ activeProjectPath?: string;
45
+ /** The original owner request that started this run. Used to populate
46
+ * PendingAgentDecision.originalRequest when a precondition fires.
47
+ * Optional — the router can fill it in later if needed. */
48
+ originalRequest?: string;
49
+ /** Stable run UUID. Stored on the decision so post-resume continues
50
+ * to reference the same run. */
51
+ runId: string;
52
+ }
53
+ export interface PreconditionGuardHandles {
54
+ /** SDK hook map. Merge into the runAgent hooks pipeline. */
55
+ hooks: Partial<Record<HookEvent, HookCallbackMatcher[]>>;
56
+ /** Read out the captured decision after the SDK stream ends. Null when
57
+ * no precondition fired during the run. */
58
+ getCapturedDecision(): PendingAgentDecision | null;
59
+ }
60
+ export declare function buildPreconditionGuardHooks(opts: PreconditionGuardOptions): PreconditionGuardHandles;
61
+ //# sourceMappingURL=precondition-guard.d.ts.map
@@ -0,0 +1,88 @@
1
+ /**
2
+ * precondition-guard — SDK PreToolUse hooks that evaluate the registered
3
+ * precondition classifiers before each tool call. When a classifier
4
+ * matches, the hook returns `permissionDecision: 'deny'` with a reason,
5
+ * captures the PendingAgentDecision in module-scoped state for the run,
6
+ * and runAgent surfaces it to the router via RunAgentResult.
7
+ *
8
+ * Why this exists
9
+ * ───────────────
10
+ * `clarification-gate.ts` already handles owner-decision routing for
11
+ * blocked external actions, but its existing BlockedActionClassifier API
12
+ * is post-hoc: the tool already ran and failed, so we parse the error,
13
+ * inferred the blocker, then asked the owner. That leaves partial state
14
+ * behind (a half-deploy, a created-but-unconfigured target, an emitted
15
+ * webhook that can't be undone).
16
+ *
17
+ * The orchestrator-first north star prefers pre-emptive gates. The SDK
18
+ * exposes PreToolUse hooks that run BEFORE every tool call regardless
19
+ * of permissionMode. We use them to short-circuit known-bad calls
20
+ * before they fire. The same PendingAgentDecision shape flows through
21
+ * the router, so the owner experience is identical — only the failure
22
+ * surface is narrower.
23
+ *
24
+ * Why PreToolUse over canUseTool
25
+ * ──────────────────────────────
26
+ * `canUseTool` is permission-prompt scoped and may be skipped under
27
+ * `bypassPermissions` mode. `PreToolUse` hooks fire universally. The
28
+ * SDK's own doc string on PermissionDeniedMessage confirms:
29
+ *
30
+ * "PreToolUse hook denies bypass canUseTool and are not covered here."
31
+ *
32
+ * That means PreToolUse decisions run FIRST, even when canUseTool is
33
+ * absent or short-circuited. PreToolUse is also already wired into the
34
+ * runAgent hooks pipeline (`tool-output-guard`, `dedup`,
35
+ * `idempotency`), so this fits the established pattern.
36
+ */
37
+ import pino from 'pino';
38
+ import { evaluatePreconditionsForToolCall, } from './clarification-gate.js';
39
+ const logger = pino({ name: 'clementine.precondition-guard' });
40
+ export function buildPreconditionGuardHooks(opts) {
41
+ let capturedDecision = null;
42
+ const env = {
43
+ cwd: opts.cwd,
44
+ ...(opts.activeProjectPath ? { activeProjectPath: opts.activeProjectPath } : {}),
45
+ };
46
+ const preToolUse = async (input, _toolUseID) => {
47
+ if (input.hook_event_name !== 'PreToolUse') {
48
+ return {};
49
+ }
50
+ const evt = input;
51
+ const toolName = String(evt.tool_name ?? 'unknown');
52
+ const toolInput = (evt.tool_input ?? {});
53
+ // Already captured a decision earlier in this run — let everything
54
+ // after pass through. The first deny interrupts the loop; subsequent
55
+ // tool calls (if any) shouldn't be re-evaluated.
56
+ if (capturedDecision)
57
+ return {};
58
+ const decision = evaluatePreconditionsForToolCall(toolName, toolInput, env, {
59
+ ...(opts.originalRequest ? { originalRequest: opts.originalRequest } : {}),
60
+ runId: opts.runId,
61
+ });
62
+ if (!decision)
63
+ return {};
64
+ capturedDecision = decision;
65
+ logger.info({
66
+ toolName,
67
+ classifierId: decision.context.classifierId,
68
+ provider: decision.context.provider,
69
+ runId: opts.runId,
70
+ }, 'precondition-guard: denied tool call pre-flight; surfacing PendingAgentDecision');
71
+ return {
72
+ hookSpecificOutput: {
73
+ hookEventName: 'PreToolUse',
74
+ permissionDecision: 'deny',
75
+ permissionDecisionReason: decision.question,
76
+ },
77
+ };
78
+ };
79
+ return {
80
+ hooks: {
81
+ PreToolUse: [{ hooks: [preToolUse] }],
82
+ },
83
+ getCapturedDecision() {
84
+ return capturedDecision;
85
+ },
86
+ };
87
+ }
88
+ //# sourceMappingURL=precondition-guard.js.map
@@ -37,6 +37,7 @@ export declare function invalidateMcpStatusEntry(name: string): {
37
37
  updatedAt: string;
38
38
  };
39
39
  import { type ToolOutputGuardConfig } from './tool-output-guard.js';
40
+ import type { PendingAgentDecision } from './clarification-gate.js';
40
41
  import type { AgentProfile } from '../types.js';
41
42
  import type { AgentManager } from './agent-manager.js';
42
43
  import type { MemoryStore } from '../memory/store.js';
@@ -158,6 +159,13 @@ export interface RunAgentResult {
158
159
  allowedToolsApplied?: string[];
159
160
  builtinToolsApplied?: string[];
160
161
  mcpServersApplied?: string[];
162
+ /** A precondition classifier fired during this run, blocking a tool
163
+ * call before it executed. The router surfaces this to the owner via
164
+ * the pendingAgentDecision flow (clarification-gate.ts). Distinct
165
+ * from the post-hoc path that runs on RunSummary.failedSideEffects —
166
+ * this path means the tool NEVER ran, no partial state was left
167
+ * behind. */
168
+ pendingAgentDecision?: PendingAgentDecision;
161
169
  }
162
170
  /**
163
171
  * Run a single agent invocation via the canonical SDK pattern.
@@ -100,6 +100,7 @@ import { buildDedupHook } from './tool-call-dedup.js';
100
100
  import { buildSideEffectIdempotencyHook } from './side-effect-idempotency.js';
101
101
  import { buildChatStopHook } from './chat-stop-hook.js';
102
102
  import { buildRunStateHooks } from './run-state.js';
103
+ import { buildPreconditionGuardHooks } from './precondition-guard.js';
103
104
  import { buildAgentMap } from './agent-definitions.js';
104
105
  import { buildExecutionToolPolicy, } from './execution-policy.js';
105
106
  const MCP_SERVER_SCRIPT = path.join(PKG_DIR, 'dist', 'tools', 'mcp-server.js');
@@ -523,9 +524,34 @@ export async function runAgent(prompt, opts) {
523
524
  },
524
525
  })
525
526
  : null;
527
+ // ── Pre-emptive blocked-action gate (Commit 3 / 1.18.211) ──────────
528
+ // Evaluates registered precondition classifiers before each tool call
529
+ // via a PreToolUse hook. When a classifier matches (e.g. `netlify
530
+ // deploy` with no `.netlify/state.json` and no `.clementine/deploy.json`),
531
+ // the tool is denied with the decision question as the reason and the
532
+ // PendingAgentDecision is captured for surfacing via RunAgentResult.
533
+ // The router stashes it on the session and asks the owner instead of
534
+ // letting the tool fire and leave partial state behind. The existing
535
+ // post-hoc BlockedActionClassifier path (clarification-gate.ts) is
536
+ // unchanged and remains as the safety net for failures the pre-call
537
+ // rule didn't anticipate.
538
+ const preconditionGuard = buildPreconditionGuardHooks({
539
+ runId,
540
+ cwd: opts.cwd ?? BASE_DIR,
541
+ // The chat path already wires `activeProject?.path` into opts.cwd
542
+ // (router.ts), so cwd doubles as the active-project hint for the
543
+ // classifier. Falling back inside the classifier keeps the public
544
+ // RunAgentOptions surface unchanged.
545
+ activeProjectPath: opts.cwd ?? BASE_DIR,
546
+ originalRequest: prompt,
547
+ });
526
548
  // Merge hook maps from the modules. SDK accepts arrays of
527
549
  // HookCallbackMatcher per event; we concatenate.
528
550
  const mergedHooks = { ...guard.hooks };
551
+ for (const [evt, matchers] of Object.entries(preconditionGuard.hooks)) {
552
+ const existing = mergedHooks[evt] ?? [];
553
+ mergedHooks[evt] = [...existing, ...matchers];
554
+ }
529
555
  for (const [evt, matchers] of Object.entries(idempotency.hooks)) {
530
556
  const existing = mergedHooks[evt] ?? [];
531
557
  mergedHooks[evt] = [...existing, ...matchers];
@@ -929,6 +955,7 @@ export async function runAgent(prompt, opts) {
929
955
  catch (err) {
930
956
  logger.debug({ err }, 'runAgent: subagent backfill failed (non-fatal)');
931
957
  }
958
+ const pendingAgentDecision = preconditionGuard.getCapturedDecision();
932
959
  return {
933
960
  text: finalText,
934
961
  totalCostUsd,
@@ -942,6 +969,7 @@ export async function runAgent(prompt, opts) {
942
969
  allowedToolsApplied: sdkAllowedTools,
943
970
  builtinToolsApplied: toolPolicy.builtinTools,
944
971
  mcpServersApplied: Object.keys(mcpServers),
972
+ ...(pendingAgentDecision ? { pendingAgentDecision } : {}),
945
973
  };
946
974
  }
947
975
  //# sourceMappingURL=run-agent.js.map
@@ -2894,6 +2894,35 @@ export class Gateway {
2894
2894
  }
2895
2895
  clearTimeout(chatTimer);
2896
2896
  clearTimeout(hardWallTimer);
2897
+ // ── Pre-emptive blocked-action decision (Commit 3 / 1.18.211) ──
2898
+ // The precondition guard inside runAgent denied a tool call
2899
+ // before it fired (e.g. `netlify deploy` without a linked site).
2900
+ // Stash the decision on the session so the next owner message
2901
+ // is parsed as a decision reply, mirror the question into the
2902
+ // transcript, and respond with the question text. No partial
2903
+ // state was left behind because the tool never ran.
2904
+ const preCallDecision = runAgentResult.pendingAgentDecision;
2905
+ if (preCallDecision && !isBuilderSession) {
2906
+ const state = this.getSession(sessionKey);
2907
+ state.pendingAgentDecision = preCallDecision;
2908
+ const decisionResponse = preCallDecision.question;
2909
+ try {
2910
+ this.assistant.injectContext(effectiveSessionKey, originalText, decisionResponse, {
2911
+ pending: false,
2912
+ model: 'chat',
2913
+ countExchange: true,
2914
+ });
2915
+ }
2916
+ catch (err) {
2917
+ logger.debug({ err }, 'chat: pre-call decision transcript mirror failed (non-fatal)');
2918
+ }
2919
+ logger.info({
2920
+ sessionKey: effectiveSessionKey,
2921
+ classifierId: preCallDecision.context.classifierId,
2922
+ provider: preCallDecision.context.provider,
2923
+ }, 'chat: precondition-guard fired pre-call; routed to PendingAgentDecision');
2924
+ return decisionResponse;
2925
+ }
2897
2926
  // Mirror transcript so memory + recall continue working — but
2898
2927
  // skip for builder sessions since their turns are spec-drafting,
2899
2928
  // not real conversation worth recalling later.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.210",
3
+ "version": "1.18.211",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",