clementine-agent 1.18.190 → 1.18.192

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.
@@ -13,7 +13,7 @@ import fs from 'node:fs';
13
13
  import path from 'node:path';
14
14
  import { query as rawQuery, listSubagents, getSubagentMessages, SYSTEM_PROMPT_DYNAMIC_BOUNDARY, } from '@anthropic-ai/claude-agent-sdk';
15
15
  import pino from 'pino';
16
- import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, PROJECTS_META_FILE, CRON_PROGRESS_DIR, CRON_REFLECTIONS_DIR, BUDGET, TASK_BUDGET_TOKENS, currentTimeZone, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, claudeCodeDisableOneMillionForModel, currentOneMillionContextMode, normalizeClaudeModelForOneMillionContext, normalizeClaudeSdkOptionsForOneMillionContext, looksLikeClaudeOneMillionContextError, envSnapshot, } from '../config.js';
16
+ import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, PROJECTS_META_FILE, CRON_PROGRESS_DIR, CRON_REFLECTIONS_DIR, BUDGET, TASK_BUDGET_TOKENS, currentTimeZone, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, claudeCodeDisableOneMillionForModel, claudeCodeSystemPrompt, currentOneMillionContextMode, normalizeClaudeModelForOneMillionContext, normalizeClaudeSdkOptionsForOneMillionContext, looksLikeClaudeOneMillionContextError, envSnapshot, } from '../config.js';
17
17
  import { summarizeIntegrationStatus } from '../config/integrations-registry.js';
18
18
  import { loadToolPreferences, computeAvailability, buildPromptInstruction, buildComposioStatusBlock, KNOWN_SERVICES, } from '../integrations/tool-preferences.js';
19
19
  import { loadClaudeIntegrations } from './mcp-bridge.js';
@@ -2841,7 +2841,10 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
2841
2841
  const stream = query({
2842
2842
  prompt: memPrompt,
2843
2843
  options: {
2844
- systemPrompt: 'You are a silent memory extraction agent. Save facts to the vault and exit.',
2844
+ // 1.18.192 preset form so Haiku call uses Claude Code subscription
2845
+ // auth. Without this, every chat exchange's auto-memory extractor
2846
+ // hit "Not logged in" silently in the background.
2847
+ systemPrompt: claudeCodeSystemPrompt('You are a silent memory extraction agent. Save facts to the vault and exit.', { minimal: true }),
2845
2848
  model: AUTO_MEMORY_MODEL,
2846
2849
  permissionMode: 'dontAsk',
2847
2850
  // MCP tool names live in allowedTools, not tools. See note at
@@ -3092,7 +3095,10 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
3092
3095
  const stream = query({
3093
3096
  prompt: reflectionPrompt,
3094
3097
  options: {
3095
- systemPrompt: 'You are a task output verifier. Assess the output quality.',
3098
+ // 1.18.192 preset form so the verifier authenticates via the
3099
+ // Claude Code subscription. Every cron-job reflection was silently
3100
+ // skipping verification before this fix.
3101
+ systemPrompt: claudeCodeSystemPrompt('You are a task output verifier. Assess the output quality.', { minimal: true }),
3096
3102
  model: MODELS.haiku,
3097
3103
  permissionMode: 'dontAsk',
3098
3104
  tools: [],
@@ -44,7 +44,7 @@ import fs from 'node:fs';
44
44
  import path from 'node:path';
45
45
  import { randomUUID } from 'node:crypto';
46
46
  import pino from 'pino';
47
- import { BASE_DIR, MODELS, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext } from '../config.js';
47
+ import { BASE_DIR, MODELS, applyOneMillionContextRecovery, claudeCodeSystemPrompt, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext } from '../config.js';
48
48
  const logger = pino({ name: 'clementine.bg-planner' });
49
49
  // ── Persistence ──────────────────────────────────────────────────────
50
50
  /**
@@ -268,12 +268,25 @@ function buildPlannerUserPrompt(opts) {
268
268
  async function runPlannerLlm(userPrompt, systemPrompt, model) {
269
269
  const { query } = await import('@anthropic-ai/claude-agent-sdk');
270
270
  let text = '';
271
+ // 1.18.192 — CRITICAL: use the `claude_code` preset for systemPrompt
272
+ // so this query uses Claude Code subscription auth (Max plan etc.).
273
+ // Raw `systemPrompt: string` tells the SDK to use API-key auth, which
274
+ // 99% of installs don't have configured — they're logged into Claude
275
+ // Code, not the Anthropic API. This was the "Not logged in · Please
276
+ // run /login" failure Ross's owner hit on 2026-05-12.
277
+ //
278
+ // The preset injects Claude Code's default system prompt; our planning
279
+ // instructions go in `append` and dominate behavior for the single
280
+ // turn (maxTurns: 1, so no agentic loop where bleed could compound).
271
281
  const stream = query({
272
282
  prompt: userPrompt,
273
283
  options: normalizeClaudeSdkOptionsForOneMillionContext({
274
284
  model,
275
285
  maxTurns: 1, // single shot — emit JSON, done
276
- systemPrompt,
286
+ // Planner uses the full preset (not `minimal`) — the planner benefits
287
+ // from knowing the working directory + git status so it can decompose
288
+ // accurately. See claudeCodeSystemPrompt() in config.ts.
289
+ systemPrompt: claudeCodeSystemPrompt(systemPrompt),
277
290
  }),
278
291
  });
279
292
  for await (const msg of stream) {
@@ -85,6 +85,13 @@ export interface BuildTurnContextOptions {
85
85
  * source/output file inventory, and deploy.json summary (if any).
86
86
  * Set by the router's resolver before the chat call. */
87
87
  activeProject?: ProjectMeta | null;
88
+ /** 1.18.191 — message shape from intent-classifier. When 'simple',
89
+ * we skip the heavyweight sections (memory recall, bg-task headlines,
90
+ * dispute gate) and only emit identity + live state. Saves ~3-4KB
91
+ * of injected tokens per routine chat turn. 'multi-step' and
92
+ * 'unknown' keep the full block. Defaults to 'unknown' = full block
93
+ * for back-compat. */
94
+ messageShape?: 'simple' | 'multi-step' | 'unknown';
88
95
  }
89
96
  export interface BuildTurnContextResult {
90
97
  /** The full ready-to-prepend context block, INCLUDING outer
@@ -68,12 +68,26 @@ export function buildClementineTurnContext(opts) {
68
68
  const parts = [];
69
69
  const nowMs = (opts.now ?? Date.now)();
70
70
  const nowDate = new Date(nowMs);
71
+ // 1.18.191 — intent-aware section gating. For 'simple' messages
72
+ // (one-line questions, casual chat, brief asks), the heavy sections
73
+ // (memory recall, bg-task headlines, dispute gate) are skipped so
74
+ // every routine turn doesn't pay 3-4KB of injected tokens for
75
+ // context the model doesn't need. Identity + live state still
76
+ // render — they're cheap and always useful. Active project still
77
+ // renders when the message references it. 'multi-step' and
78
+ // 'unknown' keep the full block.
79
+ const messageShape = opts.messageShape ?? 'unknown';
80
+ const isSimpleMessage = messageShape === 'simple';
71
81
  // 1.18.187 — detect dispute pattern (Part E). When the owner is
72
82
  // reporting a failure of prior work, we want to suppress "past
73
83
  // success" recall items (they bias the model toward defending its
74
84
  // memory instead of verifying reality) and add a verification
75
85
  // directive at the top of the block.
76
- const disputeDetected = detectDisputePattern(opts.userMessage);
86
+ // 1.18.191 also suppressed for 'simple' shape since dispute
87
+ // patterns rarely co-occur with simple messages, and the gate
88
+ // produces a multi-paragraph directive we don't want to inject
89
+ // when the message is just "what time is it".
90
+ const disputeDetected = !isSimpleMessage && detectDisputePattern(opts.userMessage);
77
91
  sections.disputeDetected = disputeDetected;
78
92
  if (disputeDetected) {
79
93
  parts.push('### Dispute mode — verification posture\n' +
@@ -103,7 +117,14 @@ export function buildClementineTurnContext(opts) {
103
117
  // hits from the SQLite memory store, scored against the user's
104
118
  // current message. Without this, Clementine has no automatic recall
105
119
  // — she'd have to spontaneously call memory_search every turn.
106
- if (opts.memoryStore?.searchContext && opts.userMessage.trim().length > 0) {
120
+ //
121
+ // 1.18.191 — skipped for 'simple' messages. "What time is it" doesn't
122
+ // need 6 memory hits injected; she has the recall tools and can call
123
+ // them on demand when the message warrants it. Saves ~2KB/turn on
124
+ // routine chat. The owner's own posture directive
125
+ // (BEHAVIORAL_POSTURE in run-agent-context.ts) tells the model to
126
+ // call memory_search proactively when relevant — that still works.
127
+ if (!isSimpleMessage && opts.memoryStore?.searchContext && opts.userMessage.trim().length > 0) {
107
128
  try {
108
129
  const hits = opts.memoryStore.searchContext(opts.userMessage, {
109
130
  limit: MAX_MEMORY_HITS,
@@ -139,7 +160,12 @@ export function buildClementineTurnContext(opts) {
139
160
  // that bias the model toward "but my memory says it succeeded."
140
161
  // Failed/aborted/interrupted tasks STAY because they're useful
141
162
  // signal for the verification posture.
142
- if (opts.listBackgroundTasks) {
163
+ //
164
+ // 1.18.191 — skipped for 'simple' messages. Routine chat doesn't
165
+ // need to know what bg tasks landed in the last 24h. If the user
166
+ // asks "what happened with X" — that's a multi-step / unknown
167
+ // shape, this section will fire normally.
168
+ if (!isSimpleMessage && opts.listBackgroundTasks) {
143
169
  try {
144
170
  const TERMINAL = disputeDetected
145
171
  ? ['failed', 'interrupted', 'aborted']
@@ -8,7 +8,7 @@
8
8
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import pino from 'pino';
11
- import { BASE_DIR, CRON_REFLECTIONS_DIR, TASKS_FILE, INBOX_DIR, MODELS, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
11
+ import { BASE_DIR, CRON_REFLECTIONS_DIR, TASKS_FILE, INBOX_DIR, MODELS, applyOneMillionContextRecovery, claudeCodeSystemPrompt, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
12
12
  import { listAllGoals } from '../tools/shared.js';
13
13
  const logger = pino({ name: 'clementine.daily-planner' });
14
14
  const PLANS_DIR = path.join(BASE_DIR, 'plans', 'daily');
@@ -256,7 +256,9 @@ Rules:
256
256
  options: normalizeClaudeSdkOptionsForOneMillionContext({
257
257
  model: MODELS.haiku,
258
258
  maxTurns: 1,
259
- systemPrompt: 'You are a planning assistant. Analyze the context and produce a prioritized daily plan as JSON. Return only valid JSON, no markdown fencing.',
259
+ // 1.18.192 preset form so SDK uses Claude Code subscription auth
260
+ // (raw string → API-key auth → "Not logged in" failure for Max users).
261
+ systemPrompt: claudeCodeSystemPrompt('You are a planning assistant. Analyze the context and produce a prioritized daily plan as JSON. Return only valid JSON, no markdown fencing.', { minimal: true }),
260
262
  }),
261
263
  });
262
264
  for await (const msg of stream) {
@@ -31,6 +31,51 @@ export declare function classifyIntent(text: string, recentExchanges?: Array<{
31
31
  * Injected into the system prompt to steer the agent's response style.
32
32
  */
33
33
  export declare function getStrategyGuidance(strategy: ResponseStrategy): string;
34
+ export type MessageShape =
35
+ /** Single ask, single response. "what time is it", "remind me to call X". */
36
+ 'simple'
37
+ /** Multiple distinct actions across phases. "send 25 emails after
38
+ * scraping data from Salesforce and SEO sources, then summarize". */
39
+ | 'multi-step'
40
+ /** Ambiguous — falls through to today's full chat path (safe default). */
41
+ | 'unknown';
42
+ export interface MessageShapeResult {
43
+ shape: MessageShape;
44
+ score: number;
45
+ reasons: string[];
46
+ }
47
+ /**
48
+ * Classify a chat message's structural shape (simple / multi-step).
49
+ *
50
+ * Scoring (sum of triggered signals):
51
+ * - 2+ shape-action verbs: +1
52
+ * - 3+ shape-action verbs: +2 cumulatively
53
+ * - each sequence marker ("and then", "after that"): +1 (up to +2)
54
+ * - batch marker ("for each", "25 emails"): +1
55
+ * - numbered list: +2
56
+ * - 2+ distinct integration domains in same message: +1
57
+ * - length > 200 chars: +1
58
+ * - length > 500 chars: +2 cumulatively
59
+ *
60
+ * Decision:
61
+ * - score >= threshold (default 3) → 'multi-step'
62
+ * - score === 0 AND <= 1 action verb AND length <= 200 → 'simple'
63
+ * - otherwise → 'unknown' (today's chat path, no change)
64
+ */
65
+ export declare function classifyMessageShape(text: string, opts?: {
66
+ threshold?: number;
67
+ }): MessageShapeResult;
68
+ /**
69
+ * Detect whether the user's message is approving / revising / canceling
70
+ * a pending plan. Used by the chat-side plan-mode state machine when
71
+ * `sess.planAwaitingApproval` is set.
72
+ *
73
+ * Conservative: only short, clearly-affirmative messages qualify as
74
+ * approval. "yes but also do X" is NOT approval — it's a revision
75
+ * request and the state machine should re-plan with the feedback.
76
+ */
77
+ export type PlanApprovalSignal = 'approve' | 'revise' | 'cancel' | 'other';
78
+ export declare function detectPlanApproval(message: string): PlanApprovalSignal;
34
79
  /**
35
80
  * Generate a follow-up suggestion prompt suffix based on completed work.
36
81
  *
@@ -188,6 +188,150 @@ No tool calls needed. Just be conversational.
188
188
  If there's relevant context from recent work or pending items, briefly mention it.`;
189
189
  }
190
190
  }
191
+ /** Action verbs that strongly suggest "do work" rather than "answer". */
192
+ const SHAPE_ACTION_VERBS = [
193
+ 'send', 'create', 'build', 'generate', 'write', 'draft', 'compose',
194
+ 'publish', 'deploy', 'upload', 'post', 'push',
195
+ 'scrape', 'fetch', 'pull', 'extract', 'gather', 'collect',
196
+ 'convert', 'merge', 'combine', 'transform', 'consolidate', 'aggregate',
197
+ 'schedule', 'queue', 'run', 'execute', 'process',
198
+ 'email', 'message', 'notify', 'alert', 'reply', 'forward',
199
+ 'import', 'export', 'sync', 'backup',
200
+ ];
201
+ const SHAPE_SEQUENCE_MARKERS = [
202
+ /\band\s+then\b/i,
203
+ /\b(?:after|once|when)\s+(?:that|you|done|finished|complete)/i,
204
+ /\b(?:then|next|finally|last)\s*[,]?\s+\w+/i,
205
+ /\bfollowed\s+by\b/i,
206
+ /\b(?:step|phase)\s+\d+/i,
207
+ ];
208
+ const SHAPE_BATCH_MARKERS = [
209
+ /\b(?:for|on|to)\s+each\b/i,
210
+ /\b\d{2,}\s+\w+/, // "25 emails", "100 records"
211
+ /\beach\s+of\s+(?:them|the)\b/i,
212
+ /\b(?:all|every)\s+(?:of\s+)?(?:them|the\s+\w+)/i,
213
+ /\b(?:bulk|batch|mass)\b/i,
214
+ ];
215
+ const SHAPE_NUMBERED_LIST = /\n\s*\d+[.)]\s+\w+/;
216
+ const SHAPE_DOMAIN_MARKERS = [
217
+ /\bsalesforce\b/i, /\bgmail\b/i, /\boutlook\b/i, /\bslack\b/i,
218
+ /\bdiscord\b/i, /\bnetlify\b/i, /\bvercel\b/i, /\bgithub\b/i,
219
+ /\bsupabase\b/i, /\bairtable\b/i, /\bhubspot\b/i, /\bnotion\b/i,
220
+ /\blinkedin\b/i, /\bcalendar\b/i, /\bdrive\b/i, /\bsheets\b/i,
221
+ ];
222
+ /**
223
+ * Classify a chat message's structural shape (simple / multi-step).
224
+ *
225
+ * Scoring (sum of triggered signals):
226
+ * - 2+ shape-action verbs: +1
227
+ * - 3+ shape-action verbs: +2 cumulatively
228
+ * - each sequence marker ("and then", "after that"): +1 (up to +2)
229
+ * - batch marker ("for each", "25 emails"): +1
230
+ * - numbered list: +2
231
+ * - 2+ distinct integration domains in same message: +1
232
+ * - length > 200 chars: +1
233
+ * - length > 500 chars: +2 cumulatively
234
+ *
235
+ * Decision:
236
+ * - score >= threshold (default 3) → 'multi-step'
237
+ * - score === 0 AND <= 1 action verb AND length <= 200 → 'simple'
238
+ * - otherwise → 'unknown' (today's chat path, no change)
239
+ */
240
+ export function classifyMessageShape(text, opts = {}) {
241
+ const reasons = [];
242
+ let score = 0;
243
+ if (!text || !text.trim()) {
244
+ return { shape: 'simple', score: 0, reasons: ['empty'] };
245
+ }
246
+ const trimmed = text.trim();
247
+ const lower = trimmed.toLowerCase();
248
+ const words = new Set(lower.replace(/[^\w\s-]/g, ' ').split(/\s+/).filter(Boolean));
249
+ // Action verbs
250
+ const matchedVerbs = [];
251
+ for (const verb of SHAPE_ACTION_VERBS) {
252
+ if (words.has(verb))
253
+ matchedVerbs.push(verb);
254
+ }
255
+ if (matchedVerbs.length >= 2) {
256
+ score += 1;
257
+ reasons.push(`2+ action verbs (${matchedVerbs.slice(0, 4).join(', ')})`);
258
+ }
259
+ if (matchedVerbs.length >= 3) {
260
+ score += 1;
261
+ reasons.push('3+ action verbs');
262
+ }
263
+ // Sequence markers
264
+ for (const rx of SHAPE_SEQUENCE_MARKERS) {
265
+ const matches = lower.match(new RegExp(rx.source, 'gi'));
266
+ if (matches && matches.length > 0) {
267
+ score += Math.min(matches.length, 2);
268
+ reasons.push(`sequence marker: ${matches[0]}`);
269
+ break;
270
+ }
271
+ }
272
+ // Batch markers
273
+ for (const rx of SHAPE_BATCH_MARKERS) {
274
+ if (rx.test(trimmed)) {
275
+ score += 1;
276
+ reasons.push('batch marker (for each / N items / bulk)');
277
+ break;
278
+ }
279
+ }
280
+ // Numbered list
281
+ if (SHAPE_NUMBERED_LIST.test(trimmed)) {
282
+ score += 2;
283
+ reasons.push('numbered list');
284
+ }
285
+ // Cross-domain
286
+ let domainCount = 0;
287
+ for (const rx of SHAPE_DOMAIN_MARKERS) {
288
+ if (rx.test(trimmed))
289
+ domainCount += 1;
290
+ }
291
+ if (domainCount >= 2) {
292
+ score += 1;
293
+ reasons.push(`${domainCount} integration domains mentioned`);
294
+ }
295
+ // Length
296
+ if (trimmed.length > 200) {
297
+ score += 1;
298
+ reasons.push(`length > 200 (${trimmed.length})`);
299
+ }
300
+ if (trimmed.length > 500) {
301
+ score += 1;
302
+ reasons.push(`length > 500`);
303
+ }
304
+ // Decision
305
+ const threshold = opts.threshold ?? 3;
306
+ let shape;
307
+ if (score >= threshold) {
308
+ shape = 'multi-step';
309
+ }
310
+ else if (score === 0 && matchedVerbs.length <= 1 && trimmed.length <= 200) {
311
+ shape = 'simple';
312
+ }
313
+ else {
314
+ shape = 'unknown';
315
+ }
316
+ return { shape, score, reasons };
317
+ }
318
+ const APPROVE_RE = /^(?:yes|y|yep|yeah|yup|sure|ok|okay|approve|approved|go|go ahead|run it|do it|sounds good|lgtm|ship it|👍|✅)[\s.!]*$/i;
319
+ const CANCEL_RE = /^(?:cancel|stop|nvm|nevermind|never\s*mind|forget it|don['']?t|abort|kill it)\b/i;
320
+ const REVISE_RE = /\b(?:but|except|instead|change|modify|add(?:\s+also)?|remove|skip|swap|wait|actually|hold on)\b/i;
321
+ export function detectPlanApproval(message) {
322
+ if (!message)
323
+ return 'other';
324
+ const text = message.trim();
325
+ if (!text)
326
+ return 'other';
327
+ if (CANCEL_RE.test(text))
328
+ return 'cancel';
329
+ if (text.length <= 30 && APPROVE_RE.test(text))
330
+ return 'approve';
331
+ if (text.length > 30 || REVISE_RE.test(text))
332
+ return 'revise';
333
+ return 'other';
334
+ }
191
335
  /**
192
336
  * Generate a follow-up suggestion prompt suffix based on completed work.
193
337
  *
@@ -10,7 +10,7 @@ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
10
10
  import os from 'node:os';
11
11
  import path from 'node:path';
12
12
  import pino from 'pino';
13
- import { BASE_DIR, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
13
+ import { BASE_DIR, applyOneMillionContextRecovery, claudeCodeSystemPrompt, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
14
14
  const logger = pino({ name: 'clementine.mcp-bridge' });
15
15
  const MCP_SERVERS_FILE = path.join(BASE_DIR, 'mcp-servers.json');
16
16
  const INTEGRATIONS_FILE = path.join(BASE_DIR, 'claude-integrations.json');
@@ -451,7 +451,8 @@ export async function probeAvailableTools(force = false) {
451
451
  const stream = query({
452
452
  prompt: 'ok',
453
453
  options: normalizeClaudeSdkOptionsForOneMillionContext({
454
- systemPrompt: 'Reply ok.',
454
+ // 1.18.192 — preset form for Claude Code subscription auth.
455
+ systemPrompt: claudeCodeSystemPrompt('Reply ok.', { minimal: true }),
455
456
  model: 'claude-haiku-4-5',
456
457
  permissionMode: 'dontAsk',
457
458
  mcpServers: externalMcpServers,
@@ -12,7 +12,7 @@
12
12
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
13
13
  import path from 'node:path';
14
14
  import pino from 'pino';
15
- import { BASE_DIR, GOALS_DIR, MODELS, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
15
+ import { BASE_DIR, GOALS_DIR, MODELS, applyOneMillionContextRecovery, claudeCodeSystemPrompt, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
16
16
  import { listAllGoals } from '../tools/shared.js';
17
17
  const logger = pino({ name: 'clementine.strategic-planner' });
18
18
  const DAILY_PLANS_DIR = path.join(BASE_DIR, 'plans', 'daily');
@@ -26,7 +26,11 @@ async function llmJsonCall(prompt, systemPrompt) {
26
26
  options: normalizeClaudeSdkOptionsForOneMillionContext({
27
27
  model: MODELS.haiku,
28
28
  maxTurns: 1,
29
- systemPrompt,
29
+ // 1.18.192 — preset form so the SDK uses Claude Code subscription auth.
30
+ // Raw `systemPrompt: string` triggers API-key auth and "Not logged in"
31
+ // failures on Max-only installs. Logs confirmed weekly review was
32
+ // silently falling through to the fallback path here since the bug landed.
33
+ systemPrompt: claudeCodeSystemPrompt(systemPrompt, { minimal: true }),
30
34
  }),
31
35
  });
32
36
  for await (const msg of stream) {
@@ -14,7 +14,7 @@ import { readFileSync } from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import pdfParse from 'pdf-parse';
16
16
  import { contentHash } from './common.js';
17
- import { MODELS, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../../config.js';
17
+ import { MODELS, applyOneMillionContextRecovery, claudeCodeSystemPrompt, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../../config.js';
18
18
  export async function* parsePdf(filePath) {
19
19
  let buf;
20
20
  try {
@@ -94,7 +94,10 @@ async function ocrPdfViaClaude(filePath) {
94
94
  options: normalizeClaudeSdkOptionsForOneMillionContext({
95
95
  model: MODELS.haiku,
96
96
  maxTurns: 4, // Read tool call + response (a few turns of thinking is fine)
97
- systemPrompt: 'You are a faithful OCR transcriber. Copy text exactly as written. When the PDF has images or scans, read the text from them using vision. Never invent content.',
97
+ // 1.18.192 preset form for Claude Code subscription auth.
98
+ // Without this, every scanned-PDF ingest hit "Not logged in" and
99
+ // silently fell back to empty OCR output.
100
+ systemPrompt: claudeCodeSystemPrompt('You are a faithful OCR transcriber. Copy text exactly as written. When the PDF has images or scans, read the text from them using vision. Never invent content.', { minimal: true }),
98
101
  // Claude Code's built-in Read tool handles PDFs (text + vision)
99
102
  tools: ['Read'],
100
103
  allowedTools: ['Read'],
@@ -19,7 +19,7 @@ import { TunnelManager } from './tunnel.js';
19
19
  import { AgentManager } from '../agent/agent-manager.js';
20
20
  import { discoverMcpServers, getClaudeIntegrations, KNOWN_MCP_DESCRIPTIONS } from '../agent/mcp-bridge.js';
21
21
  import { buildBuilderEnrichedMessage, builderSessionKey } from '../dashboard/builder/prompt.js';
22
- import { AGENTS_DIR, MEMORY_FILE, MODELS, SESSIONS_FILE, TIMEZONE, applyOneMillionContextRecovery, currentTimeZone, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, setEnvOverride, } from '../config.js';
22
+ import { AGENTS_DIR, MEMORY_FILE, MODELS, SESSIONS_FILE, TIMEZONE, applyOneMillionContextRecovery, claudeCodeSystemPrompt, currentTimeZone, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, setEnvOverride, } from '../config.js';
23
23
  import { parseTasks } from '../tools/shared.js';
24
24
  // 1.18.160 — also pull parseCronJobs + parseAgentCronJobs so getCronJobs()
25
25
  // returns the same merged set the runtime fires (CRON.md + agent CRON +
@@ -6399,7 +6399,8 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
6399
6399
  options: normalizeClaudeSdkOptionsForOneMillionContext({
6400
6400
  model: MODELS.haiku,
6401
6401
  maxTurns: 3,
6402
- systemPrompt: 'You are a data enumerator. You call the given tool once, extract the items from its response, and emit a strict JSON array. No commentary.',
6402
+ // 1.18.192 preset form for Claude Code subscription auth.
6403
+ systemPrompt: claudeCodeSystemPrompt('You are a data enumerator. You call the given tool once, extract the items from its response, and emit a strict JSON array. No commentary.', { minimal: true }),
6403
6404
  allowedTools: [tool],
6404
6405
  mcpServers,
6405
6406
  permissionMode: 'dontAsk',
@@ -9725,7 +9726,8 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
9725
9726
  options: normalizeClaudeSdkOptionsForOneMillionContext({
9726
9727
  model: 'claude-haiku-4-5-20251001',
9727
9728
  maxTurns: 1,
9728
- systemPrompt: 'You are a memory consolidation assistant. Extract only facts directly evidenced by the corpus. Be terse. Output exactly the requested format.',
9729
+ // 1.18.192 preset form for Claude Code subscription auth.
9730
+ systemPrompt: claudeCodeSystemPrompt('You are a memory consolidation assistant. Extract only facts directly evidenced by the corpus. Be terse. Output exactly the requested format.', { minimal: true }),
9729
9731
  }),
9730
9732
  });
9731
9733
  for await (const msg of stream) {
package/dist/config.d.ts CHANGED
@@ -30,6 +30,39 @@ type ClaudeSdkOptionsLike = {
30
30
  [key: string]: unknown;
31
31
  };
32
32
  export declare function normalizeClaudeSdkOptionsForOneMillionContext<T extends ClaudeSdkOptionsLike>(options: T): T;
33
+ /**
34
+ * 1.18.192 — Build a `systemPrompt` value that uses the `claude_code` preset
35
+ * so the SDK query authenticates via Claude Code subscription (Max plan,
36
+ * CLAUDE_CODE_OAUTH_TOKEN) instead of falling back to ANTHROPIC_API_KEY.
37
+ *
38
+ * Why this matters: passing `systemPrompt` as a raw string tells the SDK
39
+ * "custom prompt, no preset" — which silently routes auth to API-key mode.
40
+ * On an install with no API key (the default for Max subscribers), the
41
+ * query fails with `Error: Not logged in · Please run /login`.
42
+ *
43
+ * Hot paths affected by this bug before 1.18.192: bg-planner, daily-planner,
44
+ * strategic-planner (weekly review), auto-memory extractor, task output
45
+ * verifier, MCP tool-inventory probe, dashboard data-enumerator, dashboard
46
+ * memory-consolidator, PDF OCR, periodic memory consolidation.
47
+ *
48
+ * Use `excludeDynamicSections: true` for lightweight Haiku utility calls
49
+ * where the Claude Code preamble (working-dir, git status, memory paths)
50
+ * is irrelevant noise — saves ~1-2K input tokens per call.
51
+ *
52
+ * @example Full preset (planner, agentic work)
53
+ * systemPrompt: claudeCodeSystemPrompt('You are a planner. Emit JSON.')
54
+ *
55
+ * @example Minimal preset (single-shot Haiku utility)
56
+ * systemPrompt: claudeCodeSystemPrompt('Be terse.', { minimal: true })
57
+ */
58
+ export declare function claudeCodeSystemPrompt(append: string, opts?: {
59
+ minimal?: boolean;
60
+ }): {
61
+ type: 'preset';
62
+ preset: 'claude_code';
63
+ append: string;
64
+ excludeDynamicSections?: boolean;
65
+ };
33
66
  export declare function normalizeClaudeModelForOneMillionContext(model: string, mode?: OneMillionContextMode): string;
34
67
  export declare function usesOneMillionContext(model: string | null | undefined, mode?: OneMillionContextMode, plan?: ClaudePlan): boolean;
35
68
  /**
package/dist/config.js CHANGED
@@ -192,6 +192,39 @@ export function normalizeClaudeSdkOptionsForOneMillionContext(options) {
192
192
  }
193
193
  return next;
194
194
  }
195
+ /**
196
+ * 1.18.192 — Build a `systemPrompt` value that uses the `claude_code` preset
197
+ * so the SDK query authenticates via Claude Code subscription (Max plan,
198
+ * CLAUDE_CODE_OAUTH_TOKEN) instead of falling back to ANTHROPIC_API_KEY.
199
+ *
200
+ * Why this matters: passing `systemPrompt` as a raw string tells the SDK
201
+ * "custom prompt, no preset" — which silently routes auth to API-key mode.
202
+ * On an install with no API key (the default for Max subscribers), the
203
+ * query fails with `Error: Not logged in · Please run /login`.
204
+ *
205
+ * Hot paths affected by this bug before 1.18.192: bg-planner, daily-planner,
206
+ * strategic-planner (weekly review), auto-memory extractor, task output
207
+ * verifier, MCP tool-inventory probe, dashboard data-enumerator, dashboard
208
+ * memory-consolidator, PDF OCR, periodic memory consolidation.
209
+ *
210
+ * Use `excludeDynamicSections: true` for lightweight Haiku utility calls
211
+ * where the Claude Code preamble (working-dir, git status, memory paths)
212
+ * is irrelevant noise — saves ~1-2K input tokens per call.
213
+ *
214
+ * @example Full preset (planner, agentic work)
215
+ * systemPrompt: claudeCodeSystemPrompt('You are a planner. Emit JSON.')
216
+ *
217
+ * @example Minimal preset (single-shot Haiku utility)
218
+ * systemPrompt: claudeCodeSystemPrompt('Be terse.', { minimal: true })
219
+ */
220
+ export function claudeCodeSystemPrompt(append, opts) {
221
+ return {
222
+ type: 'preset',
223
+ preset: 'claude_code',
224
+ append,
225
+ ...(opts?.minimal ? { excludeDynamicSections: true } : {}),
226
+ };
227
+ }
195
228
  export function normalizeClaudeModelForOneMillionContext(model, mode = currentOneMillionContextMode()) {
196
229
  const family = modelFamily(model);
197
230
  if (mode === 'on')
@@ -88,6 +88,31 @@ export declare class Gateway {
88
88
  private queueBackgroundOffer;
89
89
  private formatBackgroundQueuedResponse;
90
90
  private queueBackgroundTaskAfterContextOverflow;
91
+ /**
92
+ * 1.18.191 — chat-side plan mode state machine.
93
+ *
94
+ * Two paths it handles:
95
+ *
96
+ * A. Approval-pending path. If sess.planAwaitingApproval is set,
97
+ * the user's message NOW is either approval / revision / cancel.
98
+ * Approval → dispatch the chain. Revision → re-plan with feedback.
99
+ * Cancel → clear pending state.
100
+ *
101
+ * B. Multi-step entry path. shape='multi-step' AND no pending plan:
102
+ * run planRequest synchronously, post the plan to chat asking
103
+ * for approval, set pending state. The owner's NEXT message
104
+ * advances via path A.
105
+ *
106
+ * Returns `{ handled: true, response }` when plan mode owns this turn
107
+ * (caller should return the response without running normal chat).
108
+ * Returns `{ handled: false }` to fall through to normal chat.
109
+ *
110
+ * Defensive on every external call — failures degrade to normal chat
111
+ * rather than blocking the owner's conversation.
112
+ */
113
+ private _maybeHandlePlanMode;
114
+ /** Format a plan for owner approval in chat. */
115
+ private _formatPlanForApproval;
91
116
  acceptBackgroundOffer(sessionKey: string, id: string): {
92
117
  ok: boolean;
93
118
  response: string;
@@ -469,6 +469,163 @@ export class Gateway {
469
469
  ].join('\n'),
470
470
  };
471
471
  }
472
+ /**
473
+ * 1.18.191 — chat-side plan mode state machine.
474
+ *
475
+ * Two paths it handles:
476
+ *
477
+ * A. Approval-pending path. If sess.planAwaitingApproval is set,
478
+ * the user's message NOW is either approval / revision / cancel.
479
+ * Approval → dispatch the chain. Revision → re-plan with feedback.
480
+ * Cancel → clear pending state.
481
+ *
482
+ * B. Multi-step entry path. shape='multi-step' AND no pending plan:
483
+ * run planRequest synchronously, post the plan to chat asking
484
+ * for approval, set pending state. The owner's NEXT message
485
+ * advances via path A.
486
+ *
487
+ * Returns `{ handled: true, response }` when plan mode owns this turn
488
+ * (caller should return the response without running normal chat).
489
+ * Returns `{ handled: false }` to fall through to normal chat.
490
+ *
491
+ * Defensive on every external call — failures degrade to normal chat
492
+ * rather than blocking the owner's conversation.
493
+ */
494
+ async _maybeHandlePlanMode(opts) {
495
+ const sess = this.sessions.get(opts.sessionKey);
496
+ const { detectPlanApproval } = await import('../agent/intent-classifier.js');
497
+ const { planRequest, savePlan, loadPlan } = await import('../agent/bg-planner.js');
498
+ const { dispatchChain } = await import('../agent/bg-orchestrator.js');
499
+ // ── Path A: approval-pending ────────────────────────────────────
500
+ if (sess?.planAwaitingApproval) {
501
+ const pending = sess.planAwaitingApproval;
502
+ const signal = detectPlanApproval(opts.userMessage);
503
+ logger.info({
504
+ sessionKey: opts.sessionKey,
505
+ planId: pending.planId,
506
+ chainId: pending.chainId,
507
+ signal,
508
+ }, 'Plan mode: approval signal received');
509
+ const plan = loadPlan(pending.planId, opts.activeProject?.path);
510
+ if (!plan) {
511
+ // Plan disappeared from disk — clear the pending state and
512
+ // let the message fall through to normal chat.
513
+ delete sess.planAwaitingApproval;
514
+ logger.warn({ planId: pending.planId }, 'Plan mode: pending plan not found on disk — clearing');
515
+ return { handled: false };
516
+ }
517
+ if (signal === 'approve') {
518
+ try {
519
+ const firstTask = dispatchChain(plan);
520
+ delete sess.planAwaitingApproval;
521
+ const response = [
522
+ `**Plan approved — starting step 1: ${plan.steps[0]?.title ?? '(first step)'}**`,
523
+ '',
524
+ `Background task **${firstTask.id}** is now running.`,
525
+ 'I\'ll post step-by-step updates as each step completes.',
526
+ ].join('\n');
527
+ return { handled: true, response };
528
+ }
529
+ catch (err) {
530
+ logger.warn({ err, planId: plan.id }, 'Plan mode: dispatchChain failed on approval');
531
+ return { handled: true, response: `Couldn't start the chain: ${String(err).slice(0, 200)}. Tell me to retry or try a different approach.` };
532
+ }
533
+ }
534
+ if (signal === 'cancel') {
535
+ delete sess.planAwaitingApproval;
536
+ return { handled: true, response: 'Cancelled the plan. Tell me what you\'d like to do instead.' };
537
+ }
538
+ if (signal === 'revise') {
539
+ // Re-plan with the user's revision as additional context.
540
+ try {
541
+ const revisedRequest = `${plan.userRequest}\n\n[Revision from owner: ${opts.userMessage}]`;
542
+ const newPlan = await planRequest({
543
+ userRequest: revisedRequest,
544
+ originatingSessionKey: opts.sessionKey,
545
+ ...(opts.activeProject ? { project: opts.activeProject } : {}),
546
+ });
547
+ savePlan(newPlan, newPlan.projectPath);
548
+ sess.planAwaitingApproval = {
549
+ planId: newPlan.id,
550
+ chainId: newPlan.chainId,
551
+ proposedAt: Date.now(),
552
+ };
553
+ return {
554
+ handled: true,
555
+ response: this._formatPlanForApproval(newPlan, /* revised */ true),
556
+ };
557
+ }
558
+ catch (err) {
559
+ logger.warn({ err }, 'Plan mode: revision planRequest failed');
560
+ return { handled: true, response: `Couldn't revise the plan: ${String(err).slice(0, 200)}. Want me to start fresh or try something else?` };
561
+ }
562
+ }
563
+ // signal='other' — let it fall through to normal chat, but keep
564
+ // pending state. The model will see the user message normally.
565
+ return { handled: false };
566
+ }
567
+ // ── Path B: multi-step entry ────────────────────────────────────
568
+ if (opts.shape === 'multi-step' && sess) {
569
+ try {
570
+ // Stream a "thinking..." update so the user knows planning is
571
+ // happening rather than seeing 30s of silence.
572
+ if (opts.onText) {
573
+ try {
574
+ opts.onText('🤔 Planning the steps...');
575
+ }
576
+ catch { /* non-fatal */ }
577
+ }
578
+ const plan = await planRequest({
579
+ userRequest: opts.userMessage,
580
+ originatingSessionKey: opts.sessionKey,
581
+ ...(opts.activeProject ? { project: opts.activeProject } : {}),
582
+ });
583
+ savePlan(plan, plan.projectPath);
584
+ sess.planAwaitingApproval = {
585
+ planId: plan.id,
586
+ chainId: plan.chainId,
587
+ proposedAt: Date.now(),
588
+ };
589
+ return {
590
+ handled: true,
591
+ response: this._formatPlanForApproval(plan, /* revised */ false),
592
+ };
593
+ }
594
+ catch (err) {
595
+ logger.warn({ err, sessionKey: opts.sessionKey }, 'Plan mode: planRequest failed at entry');
596
+ // Fall through to normal chat. Better than blocking the owner.
597
+ return { handled: false };
598
+ }
599
+ }
600
+ // Not a plan-mode case — fall through to normal chat.
601
+ return { handled: false };
602
+ }
603
+ /** Format a plan for owner approval in chat. */
604
+ _formatPlanForApproval(plan, revised) {
605
+ const lines = [];
606
+ lines.push(revised
607
+ ? `**Revised plan (${plan.steps.length} steps)**`
608
+ : `**Here's how I'd do this (${plan.steps.length} steps)**`);
609
+ lines.push('');
610
+ for (const step of plan.steps) {
611
+ lines.push(`${step.index + 1}. **${step.title}**`);
612
+ if (step.scope)
613
+ lines.push(` ${step.scope}`);
614
+ if (step.deliverable)
615
+ lines.push(` → ${step.deliverable}`);
616
+ }
617
+ if (plan.notes) {
618
+ lines.push('');
619
+ lines.push(`_Notes_: ${plan.notes}`);
620
+ }
621
+ if (typeof plan.estimatedCostUsd === 'number') {
622
+ lines.push('');
623
+ lines.push(`Estimated cost: ~$${plan.estimatedCostUsd.toFixed(2)}`);
624
+ }
625
+ lines.push('');
626
+ lines.push('Say **yes / approve / go** to start, **cancel** to skip, or describe a revision (e.g., "swap step 3" or "add a verification step").');
627
+ return lines.join('\n');
628
+ }
472
629
  // Offer-message formatter was removed in the Saturday-feel restoration —
473
630
  // the chat path no longer asks "want me to run this in the background?".
474
631
  // Auto-queue on explicit user intent is silent; everything else just runs.
@@ -2183,6 +2340,56 @@ export class Gateway {
2183
2340
  const { buildClementineTurnContext } = await import('../agent/clementine-turn-context.js');
2184
2341
  const { listBackgroundTasks } = await import('../agent/background-tasks.js');
2185
2342
  const { resolveProjectFromMessage } = await import('../agent/project-resolver.js');
2343
+ const { classifyMessageShape } = await import('../agent/intent-classifier.js');
2344
+ // 1.18.191 — classify message shape early. Simple messages
2345
+ // get a lean turn-context block (no memory recall, no bg
2346
+ // headlines, no dispute gate); multi-step messages keep
2347
+ // the full block AND may trigger plan mode (below).
2348
+ // Builder sessions skip — they have their own routing.
2349
+ const shapeResult = !isBuilderSession
2350
+ ? classifyMessageShape(originalText)
2351
+ : { shape: 'simple', score: 0, reasons: ['builder-session'] };
2352
+ logger.debug({
2353
+ sessionKey: effectiveSessionKey,
2354
+ shape: shapeResult.shape,
2355
+ score: shapeResult.score,
2356
+ reasons: shapeResult.reasons,
2357
+ }, 'Message shape classified');
2358
+ // 1.18.191 — plan mode state machine.
2359
+ //
2360
+ // Two entry points for plan mode:
2361
+ //
2362
+ // A. Approval-pending path. If the previous turn proposed a
2363
+ // plan, the user's message NOW is either approval,
2364
+ // revision, or cancel. Handle and return without
2365
+ // running normal chat.
2366
+ //
2367
+ // B. Multi-step entry path. If shape='multi-step' AND no
2368
+ // pending plan, generate a plan, post it, set pending
2369
+ // state, return. The owner's NEXT message advances via
2370
+ // path A.
2371
+ //
2372
+ // Builder sessions skip both paths.
2373
+ if (!isBuilderSession) {
2374
+ const planMode = await this._maybeHandlePlanMode({
2375
+ sessionKey: effectiveSessionKey,
2376
+ userMessage: originalText,
2377
+ shape: shapeResult.shape,
2378
+ activeProject: this.getSessionProject(effectiveSessionKey) ?? null,
2379
+ onText: wrappedOnText,
2380
+ });
2381
+ if (planMode.handled) {
2382
+ clearTimeout(chatTimer);
2383
+ if (hardWallTimer)
2384
+ clearTimeout(hardWallTimer);
2385
+ {
2386
+ const cs = this.sessions.get(sessionKey);
2387
+ if (cs)
2388
+ delete cs.abortController;
2389
+ }
2390
+ return planMode.response;
2391
+ }
2392
+ }
2186
2393
  // 1.18.187 — auto-resolve project from the user's message.
2187
2394
  // If a linked project's name/keyword matches with high
2188
2395
  // confidence, set sess.project for this turn so cwd shifts,
@@ -2294,6 +2501,10 @@ export class Gateway {
2294
2501
  // 1.18.187 — pass active project so the turn-context block
2295
2502
  // can include path / STATUS.md / inventory / deploy config.
2296
2503
  activeProject: this.getSessionProject(effectiveSessionKey) ?? null,
2504
+ // 1.18.191 — pass message shape so simple messages get the
2505
+ // lean turn-context (skip memory recall, bg-task headlines,
2506
+ // dispute gate). Token-optimization for routine chat.
2507
+ messageShape: shapeResult.shape,
2297
2508
  });
2298
2509
  clementineContextBlock = turnCtx.block;
2299
2510
  logger.debug({
package/dist/index.js CHANGED
@@ -635,7 +635,9 @@ async function asyncMain() {
635
635
  options: config.normalizeClaudeSdkOptionsForOneMillionContext({
636
636
  model: 'claude-haiku-4-5-20251001',
637
637
  maxTurns: 1,
638
- systemPrompt: 'You are a memory consolidation assistant. Be concise.',
638
+ // 1.18.192 preset form so SDK uses Claude Code subscription
639
+ // auth (raw string → API-key path → "Not logged in" for Max users).
640
+ systemPrompt: config.claudeCodeSystemPrompt('You are a memory consolidation assistant. Be concise.', { minimal: true }),
639
641
  }),
640
642
  });
641
643
  for await (const msg of stream) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.190",
3
+ "version": "1.18.192",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",