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.
- package/dist/agent/assistant.js +9 -3
- package/dist/agent/bg-planner.js +15 -2
- package/dist/agent/clementine-turn-context.d.ts +7 -0
- package/dist/agent/clementine-turn-context.js +29 -3
- package/dist/agent/daily-planner.js +4 -2
- package/dist/agent/intent-classifier.d.ts +45 -0
- package/dist/agent/intent-classifier.js +144 -0
- package/dist/agent/mcp-bridge.js +3 -2
- package/dist/agent/strategic-planner.js +6 -2
- package/dist/brain/adapters/pdf.js +5 -2
- package/dist/cli/dashboard.js +5 -3
- package/dist/config.d.ts +33 -0
- package/dist/config.js +33 -0
- package/dist/gateway/router.d.ts +25 -0
- package/dist/gateway/router.js +211 -0
- package/dist/index.js +3 -1
- package/package.json +1 -1
package/dist/agent/assistant.js
CHANGED
|
@@ -13,7 +13,7 @@ import fs from 'node:fs';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { query as rawQuery, listSubagents, getSubagentMessages, SYSTEM_PROMPT_DYNAMIC_BOUNDARY, } from '@anthropic-ai/claude-agent-sdk';
|
|
15
15
|
import pino from 'pino';
|
|
16
|
-
import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, PROJECTS_META_FILE, CRON_PROGRESS_DIR, CRON_REFLECTIONS_DIR, BUDGET, TASK_BUDGET_TOKENS, 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
|
-
|
|
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
|
-
|
|
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: [],
|
package/dist/agent/bg-planner.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
*
|
package/dist/agent/mcp-bridge.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'],
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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')
|
package/dist/gateway/router.d.ts
CHANGED
|
@@ -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;
|
package/dist/gateway/router.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|