@bike4mind/cli 0.4.0 → 0.6.0
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/{ConfigStore-DBUmvCfe.mjs → ConfigStore-Dt6utdSA.mjs} +53 -2
- package/dist/commands/doctorCommand.mjs +1 -1
- package/dist/commands/headlessCommand.mjs +2 -2
- package/dist/commands/mcpCommand.mjs +1 -1
- package/dist/commands/updateCommand.mjs +1 -1
- package/dist/index.mjs +326 -9
- package/dist/{tools-DiSJh1Dv.mjs → tools-dIVxi-6-.mjs} +480 -114
- package/dist/{updateChecker-DcJC1C8S.mjs → updateChecker-DZXWZfKF.mjs} +1 -1
- package/package.json +19 -19
|
@@ -1027,6 +1027,8 @@ const SreRepoConfigSchema = z.object({
|
|
|
1027
1027
|
maxFixesPerDay: z.number().min(0).default(5),
|
|
1028
1028
|
/** Max revision attempts before escalating to human */
|
|
1029
1029
|
maxRevisions: z.number().int().min(0).max(10).default(2),
|
|
1030
|
+
/** Max CI retry attempts before permanently failing (typecheck/apply-fix failures) */
|
|
1031
|
+
maxCiRetries: z.number().int().min(0).max(3).default(1),
|
|
1030
1032
|
/** Log actions without dispatching */
|
|
1031
1033
|
dryRun: z.boolean().default(false),
|
|
1032
1034
|
/** Comma-separated GitHub usernames to request as PR reviewers */
|
|
@@ -1035,6 +1037,12 @@ const SreRepoConfigSchema = z.object({
|
|
|
1035
1037
|
defaultBranch: z.string().default(""),
|
|
1036
1038
|
/** Build command for the workflow */
|
|
1037
1039
|
buildCommand: z.string().default(""),
|
|
1040
|
+
/**
|
|
1041
|
+
* Repository-specific instructions for the Diagnostician (max 2,000 chars).
|
|
1042
|
+
* For critical constraints that would cause broken fixes if unknown — e.g., "never modify X",
|
|
1043
|
+
* "all DB calls must go through Y wrapper". Not for general coding conventions (use CLAUDE.md).
|
|
1044
|
+
*/
|
|
1045
|
+
sreInstructions: z.string().max(2e3).default(""),
|
|
1038
1046
|
/** Files the Surgeon can modify (glob patterns). Base patterns always merged in. */
|
|
1039
1047
|
allowedFilePatterns: z.array(z.string()).default([]),
|
|
1040
1048
|
/** Files never auto-fixed (glob patterns) */
|
|
@@ -1062,11 +1070,11 @@ const SreRepoConfigSchema = z.object({
|
|
|
1062
1070
|
/** Token budget per analysis */
|
|
1063
1071
|
tokenBudget: z.object({
|
|
1064
1072
|
maxInputTokens: z.number().default(5e4),
|
|
1065
|
-
maxOutputTokens: z.number().default(
|
|
1073
|
+
maxOutputTokens: z.number().default(16e3),
|
|
1066
1074
|
maxGithubApiCalls: z.number().default(20)
|
|
1067
1075
|
}).default({
|
|
1068
1076
|
maxInputTokens: 5e4,
|
|
1069
|
-
maxOutputTokens:
|
|
1077
|
+
maxOutputTokens: 16e3,
|
|
1070
1078
|
maxGithubApiCalls: 20
|
|
1071
1079
|
}),
|
|
1072
1080
|
/** Error sources */
|
|
@@ -6280,6 +6288,49 @@ let FriendshipEvents = /* @__PURE__ */ function(FriendshipEvents) {
|
|
|
6280
6288
|
FriendshipEvents["FRIENDSHIP_CANCEL"] = "Friendship Cancelled";
|
|
6281
6289
|
return FriendshipEvents;
|
|
6282
6290
|
}({});
|
|
6291
|
+
/**
|
|
6292
|
+
* Overwatch Analytics Event Schema
|
|
6293
|
+
*
|
|
6294
|
+
* Cross-product event schema for the Overwatch marketing command center.
|
|
6295
|
+
* Products (VibesWire, B4M, StocksAndVibes, K2Kanji) emit these events
|
|
6296
|
+
* to a shared SQS queue. Overwatch consumes them and rolls up DAU/WAU/MAU.
|
|
6297
|
+
*
|
|
6298
|
+
* Unlike the B4M-internal IBaseEvent analytics events, this schema is
|
|
6299
|
+
* designed for cross-product use with Zod validation at both emission
|
|
6300
|
+
* and consumption boundaries.
|
|
6301
|
+
*/
|
|
6302
|
+
const OverwatchUtmSchema = z.object({
|
|
6303
|
+
source: z.string().max(128).optional(),
|
|
6304
|
+
medium: z.string().max(128).optional(),
|
|
6305
|
+
campaign: z.string().max(128).optional(),
|
|
6306
|
+
content: z.string().max(128).optional()
|
|
6307
|
+
});
|
|
6308
|
+
z.object({
|
|
6309
|
+
/** UUID for deduplication (SQS is at-least-once) */
|
|
6310
|
+
eventId: z.string().uuid(),
|
|
6311
|
+
/** Schema version for forward compatibility */
|
|
6312
|
+
schemaVersion: z.number().int().positive(),
|
|
6313
|
+
/** Product identifier: 'vibeswire', 'bike4mind', 'stocksandvibes', 'k2kanji', etc. */
|
|
6314
|
+
productId: z.string().min(1).max(64),
|
|
6315
|
+
/** Product's internal user ID */
|
|
6316
|
+
userId: z.string().min(1).max(256),
|
|
6317
|
+
/** Session identifier for retention/funnel analysis */
|
|
6318
|
+
sessionId: z.string().min(1).max(256),
|
|
6319
|
+
/** Event type: 'session_start', 'signup', 'feature_used', etc. */
|
|
6320
|
+
event: z.string().min(1).max(128),
|
|
6321
|
+
/** ISO 8601 timestamp */
|
|
6322
|
+
timestamp: z.string().datetime(),
|
|
6323
|
+
/** Where the user came from */
|
|
6324
|
+
referrer: z.string().url().refine((url) => /^https?:\/\//i.test(url), "referrer must be an http or https URL").max(2048).optional(),
|
|
6325
|
+
/** UTM attribution parameters */
|
|
6326
|
+
utm: OverwatchUtmSchema.optional(),
|
|
6327
|
+
/** Event-specific key-value data. Flat values only, max 1KB serialized. */
|
|
6328
|
+
metadata: z.record(z.string(), z.union([
|
|
6329
|
+
z.string(),
|
|
6330
|
+
z.number(),
|
|
6331
|
+
z.boolean()
|
|
6332
|
+
])).refine((v) => JSON.stringify(v).length <= 1024, "metadata must be ≤ 1KB serialized").optional()
|
|
6333
|
+
});
|
|
6283
6334
|
const InternalTeamMemberSchema = z.object({
|
|
6284
6335
|
name: z.string().min(1, "Name is required"),
|
|
6285
6336
|
phone: z.string().min(1, "Phone is required"),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { i as version, n as fetchLatestVersion, r as forceCheckForUpdate } from "../updateChecker-
|
|
2
|
+
import { i as version, n as fetchLatestVersion, r as forceCheckForUpdate } from "../updateChecker-DZXWZfKF.mjs";
|
|
3
3
|
import { execSync } from "child_process";
|
|
4
4
|
import { constants, existsSync, promises } from "fs";
|
|
5
5
|
import { homedir } from "os";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { n as logger, t as ConfigStore } from "../ConfigStore-
|
|
2
|
+
import { A as getApiUrl, C as WebSocketLlmBackend, G as isReadOnlyTool, J as CheckpointStore, K as ReActAgent, M as PermissionManager, N as generateCliTools, S as FallbackLlmBackend, T as McpManager, U as buildCoreSystemPrompt, V as setWebSocketToolExecutor, X as SessionStore, _ as createSkillTool, b as WebSocketToolExecutor, c as createFindDefinitionTool, d as createCoordinateTaskTool, f as createBackgroundAgentTools, g as SubagentOrchestrator, h as AgentStore, k as loadContextFiles, l as createTodoStore, m as createAgentDelegateTool, p as BackgroundAgentManager, q as CustomCommandStore, s as createGetFileStructureTool, u as createWriteTodosTool, w as ServerLlmBackend, x as WebSocketConnectionManager, y as ApiClient } from "../tools-dIVxi-6-.mjs";
|
|
3
|
+
import { n as logger, t as ConfigStore } from "../ConfigStore-Dt6utdSA.mjs";
|
|
4
4
|
import { t as DEFAULT_SANDBOX_CONFIG } from "../types-DBEjF9YS.mjs";
|
|
5
5
|
import { t as createSandboxRuntime } from "../SandboxRuntimeAdapter-C1B4t20N.mjs";
|
|
6
6
|
import { t as SandboxOrchestrator } from "../SandboxOrchestrator-BEW3rqYi.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { i as version, r as forceCheckForUpdate } from "../updateChecker-
|
|
2
|
+
import { i as version, r as forceCheckForUpdate } from "../updateChecker-DZXWZfKF.mjs";
|
|
3
3
|
import { execSync } from "child_process";
|
|
4
4
|
//#region src/commands/updateCommand.ts
|
|
5
5
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { n as useCliStore, t as selectActiveBackgroundAgents } from "./store-B7-LLvvx.mjs";
|
|
3
|
-
import { A as
|
|
4
|
-
import { Mt as validateNotebookPath$1, g as ChatModels, jt as validateJupyterKernelName, m as CREDIT_DEDUCT_TRANSACTION_TYPES, n as logger, t as ConfigStore } from "./ConfigStore-
|
|
5
|
-
import { i as version, t as checkForUpdate } from "./updateChecker-
|
|
3
|
+
import { $ as processFileReferences, A as getApiUrl, B as registerFeatureModuleTools, C as WebSocketLlmBackend, D as formatStep, E as substituteArguments, F as DEFAULT_AGENT_MODEL, G as isReadOnlyTool, H as OllamaBackend, I as DEFAULT_MAX_ITERATIONS, J as CheckpointStore, K as ReActAgent, L as DEFAULT_RETRY_CONFIG, M as PermissionManager, N as generateCliTools, O as extractCompactInstructions, P as ALWAYS_DENIED_FOR_AGENTS, Q as hasFileReferences, R as DEFAULT_THOROUGHNESS, S as FallbackLlmBackend, T as McpManager, U as buildCoreSystemPrompt, V as setWebSocketToolExecutor, W as buildSkillsPromptSection, X as SessionStore, Y as CommandHistoryStore, Z as OAuthClient, _ as createSkillTool, a as createDecisionStore, b as WebSocketToolExecutor, c as createFindDefinitionTool, d as createCoordinateTaskTool, et as searchCommands, f as createBackgroundAgentTools, g as SubagentOrchestrator, h as AgentStore, i as createDecisionLogTool, it as warmFileCache, j as getEnvironmentName, k as loadContextFiles, l as createTodoStore, m as createAgentDelegateTool, n as createBlockerTools, nt as formatFileSize, o as formatDecisionsOutput, p as BackgroundAgentManager, q as CustomCommandStore, r as formatBlockersOutput, rt as searchFiles, s as createGetFileStructureTool, t as createBlockerStore, tt as mergeCommands, u as createWriteTodosTool, v as parseAgentConfig, w as ServerLlmBackend, x as WebSocketConnectionManager, y as ApiClient, z as clearFeatureModuleTools } from "./tools-dIVxi-6-.mjs";
|
|
4
|
+
import { Mt as validateNotebookPath$1, g as ChatModels, jt as validateJupyterKernelName, m as CREDIT_DEDUCT_TRANSACTION_TYPES, n as logger, t as ConfigStore } from "./ConfigStore-Dt6utdSA.mjs";
|
|
5
|
+
import { i as version, t as checkForUpdate } from "./updateChecker-DZXWZfKF.mjs";
|
|
6
6
|
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
7
7
|
import { Box, Static, Text, render, useApp, useInput } from "ink";
|
|
8
8
|
import { execSync } from "child_process";
|
|
@@ -2484,10 +2484,207 @@ function createCompactedSession(originalSession, summary, preservedMessages) {
|
|
|
2484
2484
|
totalTokens: 0,
|
|
2485
2485
|
totalCost: 0,
|
|
2486
2486
|
toolCallCount: 0,
|
|
2487
|
-
compactedFrom: originalSession.id
|
|
2487
|
+
compactedFrom: originalSession.id,
|
|
2488
|
+
...originalSession.metadata.workflow ? { workflow: originalSession.metadata.workflow } : {}
|
|
2488
2489
|
}
|
|
2489
2490
|
};
|
|
2490
2491
|
}
|
|
2492
|
+
/**
|
|
2493
|
+
* Prefix tag used to mark a system message as an injected handoff. Kept as a
|
|
2494
|
+
* single source of truth so the dedup-on-resume check and the system-message
|
|
2495
|
+
* builder cannot drift out of sync.
|
|
2496
|
+
*/
|
|
2497
|
+
const HANDOFF_MARKER = "[Session handoff from previous session]";
|
|
2498
|
+
const MAX_MESSAGE_CHARS = 2e3;
|
|
2499
|
+
/**
|
|
2500
|
+
* Cap on the number of conversation messages included in the handoff prompt.
|
|
2501
|
+
* Prevents unbounded prompt growth on long sessions; the most recent messages
|
|
2502
|
+
* are kept since they best reflect the session's current state. Decisions and
|
|
2503
|
+
* blockers from workflow state are still included in full above the excerpt.
|
|
2504
|
+
*/
|
|
2505
|
+
const MAX_CONVERSATION_MESSAGES = 50;
|
|
2506
|
+
const ROLE_LABELS = {
|
|
2507
|
+
user: "User",
|
|
2508
|
+
assistant: "Assistant",
|
|
2509
|
+
system: "System"
|
|
2510
|
+
};
|
|
2511
|
+
/**
|
|
2512
|
+
* Build a prompt instructing the LLM to produce a structured session handoff
|
|
2513
|
+
* as JSON. Incorporates decisions and blockers from the existing workflow state
|
|
2514
|
+
* so the handoff reflects durable state, not just chat history.
|
|
2515
|
+
*
|
|
2516
|
+
* Any previously-injected handoff message (from a prior /resume) is filtered
|
|
2517
|
+
* out — the LLM should produce a fresh handoff from the actual conversation,
|
|
2518
|
+
* not echo back a prior handoff sitting at the top of the message list.
|
|
2519
|
+
*
|
|
2520
|
+
* Returns an empty string for short sessions — callers should skip generation.
|
|
2521
|
+
*/
|
|
2522
|
+
function buildHandoffPrompt(session) {
|
|
2523
|
+
if (session.messages.length < 4) return "";
|
|
2524
|
+
const filtered = session.messages.filter((m) => !isInjectedHandoff(m));
|
|
2525
|
+
const conversation = filtered.length > MAX_CONVERSATION_MESSAGES ? filtered.slice(-MAX_CONVERSATION_MESSAGES) : filtered;
|
|
2526
|
+
let prompt = `You are generating a structured session handoff so the next session (or another agent) can pick up seamlessly without re-reading the full chat history.
|
|
2527
|
+
|
|
2528
|
+
Output a single JSON object — no prose, no markdown fences — with exactly these fields:
|
|
2529
|
+
|
|
2530
|
+
{
|
|
2531
|
+
"summary": "2-4 sentence overview of what this session accomplished and where it ended",
|
|
2532
|
+
"keyFindings": ["concise factual discoveries — e.g. 'auth bug is in middleware.ts:42, caused by missing token refresh'"],
|
|
2533
|
+
"nextSteps": ["concrete actions the next session should take, in priority order"],
|
|
2534
|
+
"pendingDecisions": ["open questions or trade-offs awaiting a decision"],
|
|
2535
|
+
"blockers": ["anything preventing progress — wait conditions, missing inputs, broken upstream"]
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
Rules:
|
|
2539
|
+
- Each list item is a single line, no nested structure.
|
|
2540
|
+
- Empty lists are fine — use [] when nothing applies.
|
|
2541
|
+
- Be specific: cite filenames, function names, error messages where relevant.
|
|
2542
|
+
- Do not invent context. Only include items grounded in the conversation or workflow state below.
|
|
2543
|
+
|
|
2544
|
+
`;
|
|
2545
|
+
prompt += appendWorkflowContext(session.metadata.workflow);
|
|
2546
|
+
prompt += `CONVERSATION:\n\n`;
|
|
2547
|
+
for (const msg of conversation) {
|
|
2548
|
+
const role = ROLE_LABELS[msg.role] || "System";
|
|
2549
|
+
const content = msg.content.length > MAX_MESSAGE_CHARS ? msg.content.slice(0, MAX_MESSAGE_CHARS) + "...[truncated]" : msg.content;
|
|
2550
|
+
prompt += `**${role}:** ${content}\n\n`;
|
|
2551
|
+
}
|
|
2552
|
+
prompt += `\nReturn only the JSON object.`;
|
|
2553
|
+
return prompt;
|
|
2554
|
+
}
|
|
2555
|
+
function appendWorkflowContext(workflow) {
|
|
2556
|
+
if (!workflow) return "";
|
|
2557
|
+
const sections = [];
|
|
2558
|
+
if (workflow.decisions.length > 0) {
|
|
2559
|
+
const lines = workflow.decisions.map((d) => `- ${d.summary} (rationale: ${d.rationale})`);
|
|
2560
|
+
sections.push(`LOGGED DECISIONS:\n${lines.join("\n")}`);
|
|
2561
|
+
}
|
|
2562
|
+
const openBlockers = workflow.blockers.filter((b) => b.status === "open");
|
|
2563
|
+
if (openBlockers.length > 0) {
|
|
2564
|
+
const lines = openBlockers.map((b) => `- ${b.description}`);
|
|
2565
|
+
sections.push(`OPEN BLOCKERS:\n${lines.join("\n")}`);
|
|
2566
|
+
}
|
|
2567
|
+
return sections.length > 0 ? `${sections.join("\n\n")}\n\n` : "";
|
|
2568
|
+
}
|
|
2569
|
+
/**
|
|
2570
|
+
* Parse a raw LLM response into a SessionHandoff.
|
|
2571
|
+
*
|
|
2572
|
+
* Tolerates fenced code blocks (```json ... ```) and surrounding prose by
|
|
2573
|
+
* extracting the first balanced JSON object. Returns null if no valid handoff
|
|
2574
|
+
* can be parsed — callers decide how to surface that to the user.
|
|
2575
|
+
*/
|
|
2576
|
+
function parseHandoffResponse(response) {
|
|
2577
|
+
const json = extractJsonObject(response);
|
|
2578
|
+
if (!json) return null;
|
|
2579
|
+
let parsed;
|
|
2580
|
+
try {
|
|
2581
|
+
parsed = JSON.parse(json);
|
|
2582
|
+
} catch {
|
|
2583
|
+
return null;
|
|
2584
|
+
}
|
|
2585
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
2586
|
+
const obj = parsed;
|
|
2587
|
+
const summary = typeof obj.summary === "string" ? obj.summary.trim() : "";
|
|
2588
|
+
if (!summary) return null;
|
|
2589
|
+
return {
|
|
2590
|
+
summary,
|
|
2591
|
+
keyFindings: toStringArray(obj.keyFindings),
|
|
2592
|
+
nextSteps: toStringArray(obj.nextSteps),
|
|
2593
|
+
pendingDecisions: toStringArray(obj.pendingDecisions),
|
|
2594
|
+
blockers: toStringArray(obj.blockers),
|
|
2595
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
function toStringArray(value) {
|
|
2599
|
+
if (!Array.isArray(value)) return [];
|
|
2600
|
+
return value.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim());
|
|
2601
|
+
}
|
|
2602
|
+
function extractJsonObject(text) {
|
|
2603
|
+
const trimmed = text.trim();
|
|
2604
|
+
if (!trimmed) return null;
|
|
2605
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
2606
|
+
const candidate = fenced ? fenced[1].trim() : trimmed;
|
|
2607
|
+
const start = candidate.indexOf("{");
|
|
2608
|
+
if (start === -1) return null;
|
|
2609
|
+
let depth = 0;
|
|
2610
|
+
let inString = false;
|
|
2611
|
+
let escaped = false;
|
|
2612
|
+
for (let i = start; i < candidate.length; i++) {
|
|
2613
|
+
const ch = candidate[i];
|
|
2614
|
+
if (escaped) {
|
|
2615
|
+
escaped = false;
|
|
2616
|
+
continue;
|
|
2617
|
+
}
|
|
2618
|
+
if (ch === "\\" && inString) {
|
|
2619
|
+
escaped = true;
|
|
2620
|
+
continue;
|
|
2621
|
+
}
|
|
2622
|
+
if (ch === "\"") {
|
|
2623
|
+
inString = !inString;
|
|
2624
|
+
continue;
|
|
2625
|
+
}
|
|
2626
|
+
if (inString) continue;
|
|
2627
|
+
if (ch === "{") depth++;
|
|
2628
|
+
else if (ch === "}") {
|
|
2629
|
+
depth--;
|
|
2630
|
+
if (depth === 0) return candidate.slice(start, i + 1);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
return null;
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Render a SessionHandoff for terminal display, including the generation
|
|
2637
|
+
* timestamp so the user knows how fresh the context is.
|
|
2638
|
+
*/
|
|
2639
|
+
function formatHandoffOutput(handoff) {
|
|
2640
|
+
return formatHandoff(handoff, { includeTimestamp: true });
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Build the system message that injects handoff context into a resumed
|
|
2644
|
+
* session. Excludes the timestamp so the message text is stable across
|
|
2645
|
+
* regenerations — important for keeping LLM prompt caches warm.
|
|
2646
|
+
*/
|
|
2647
|
+
function buildHandoffSystemMessage(handoff) {
|
|
2648
|
+
return `${HANDOFF_MARKER}\n\n${formatHandoff(handoff, { includeTimestamp: false })}`;
|
|
2649
|
+
}
|
|
2650
|
+
function formatHandoff(handoff, options) {
|
|
2651
|
+
const lines = [handoff.summary, ""];
|
|
2652
|
+
appendSection(lines, "Key findings", handoff.keyFindings);
|
|
2653
|
+
appendSection(lines, "Next steps", handoff.nextSteps);
|
|
2654
|
+
appendSection(lines, "Pending decisions", handoff.pendingDecisions);
|
|
2655
|
+
appendSection(lines, "Blockers", handoff.blockers);
|
|
2656
|
+
if (options.includeTimestamp) lines.push(`Generated: ${handoff.generatedAt}`);
|
|
2657
|
+
else if (lines[lines.length - 1] === "") lines.pop();
|
|
2658
|
+
return lines.join("\n");
|
|
2659
|
+
}
|
|
2660
|
+
function appendSection(lines, heading, items) {
|
|
2661
|
+
if (items.length === 0) return;
|
|
2662
|
+
lines.push(`${heading}:`);
|
|
2663
|
+
for (const item of items) lines.push(` - ${item}`);
|
|
2664
|
+
lines.push("");
|
|
2665
|
+
}
|
|
2666
|
+
/**
|
|
2667
|
+
* True when a message is a previously-injected handoff system message.
|
|
2668
|
+
* Used to deduplicate handoff injections across save/resume cycles.
|
|
2669
|
+
*/
|
|
2670
|
+
function isInjectedHandoff(message) {
|
|
2671
|
+
return message.role === "system" && message.content.startsWith("[Session handoff from previous session]");
|
|
2672
|
+
}
|
|
2673
|
+
/**
|
|
2674
|
+
* Return a new message list with the handoff prepended as a system message.
|
|
2675
|
+
* Any previously-injected handoff anywhere in the list is removed so the
|
|
2676
|
+
* message list stays stable across repeated save/resume cycles. We scan the
|
|
2677
|
+
* whole list rather than just index 0 because compaction can prepend a
|
|
2678
|
+
* summary system message, pushing the prior handoff to a later index.
|
|
2679
|
+
*/
|
|
2680
|
+
function injectHandoffMessage(messages, handoff) {
|
|
2681
|
+
return [{
|
|
2682
|
+
id: v4(),
|
|
2683
|
+
role: "system",
|
|
2684
|
+
content: buildHandoffSystemMessage(handoff),
|
|
2685
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2686
|
+
}, ...messages.filter((m) => !isInjectedHandoff(m))];
|
|
2687
|
+
}
|
|
2491
2688
|
//#endregion
|
|
2492
2689
|
//#region src/utils/imageRenderer.ts
|
|
2493
2690
|
/**
|
|
@@ -5036,6 +5233,8 @@ function CliApp() {
|
|
|
5036
5233
|
const [initError, setInitError] = useState(null);
|
|
5037
5234
|
const [commandHistory, setCommandHistory] = useState([]);
|
|
5038
5235
|
const imageStoreInitPromise = useRef(null);
|
|
5236
|
+
const decisionStoreRef = useRef(createDecisionStore());
|
|
5237
|
+
const blockerStoreRef = useRef(createBlockerStore());
|
|
5039
5238
|
const setStoreSession = useCliStore((state) => state.setSession);
|
|
5040
5239
|
const enqueuePermissionPrompt = useCliStore((state) => state.enqueuePermissionPrompt);
|
|
5041
5240
|
const enqueueUserQuestionPrompt = useCliStore((state) => state.enqueueUserQuestionPrompt);
|
|
@@ -5478,6 +5677,15 @@ function CliApp() {
|
|
|
5478
5677
|
logger.warn(`⚠️ Model "${fromModel}" failed (${error.message}) — falling back to "${toModel}"`);
|
|
5479
5678
|
}) : llm, backgroundManager);
|
|
5480
5679
|
const writeTodosTool = createWriteTodosTool(createTodoStore());
|
|
5680
|
+
const decisionLogTool = createDecisionLogTool(decisionStoreRef.current);
|
|
5681
|
+
const blockerTools = createBlockerTools(blockerStoreRef.current);
|
|
5682
|
+
if (newSession.metadata.workflow) {
|
|
5683
|
+
decisionStoreRef.current.decisions = [...newSession.metadata.workflow.decisions];
|
|
5684
|
+
blockerStoreRef.current.blockers = [...newSession.metadata.workflow.blockers];
|
|
5685
|
+
} else {
|
|
5686
|
+
decisionStoreRef.current.decisions = [];
|
|
5687
|
+
blockerStoreRef.current.blockers = [];
|
|
5688
|
+
}
|
|
5481
5689
|
const enableSkillTool = config.preferences.enableSkillTool !== false;
|
|
5482
5690
|
const skillTool = enableSkillTool ? createSkillTool({
|
|
5483
5691
|
customCommandStore: state.customCommandStore,
|
|
@@ -5495,6 +5703,8 @@ function CliApp() {
|
|
|
5495
5703
|
agentDelegateTool,
|
|
5496
5704
|
...backgroundTools,
|
|
5497
5705
|
writeTodosTool,
|
|
5706
|
+
decisionLogTool,
|
|
5707
|
+
...blockerTools,
|
|
5498
5708
|
findDefinitionTool,
|
|
5499
5709
|
getFileStructureTool
|
|
5500
5710
|
];
|
|
@@ -5924,7 +6134,13 @@ function CliApp() {
|
|
|
5924
6134
|
...currentSession.metadata,
|
|
5925
6135
|
totalTokens: currentSession.metadata.totalTokens + result.completionInfo.totalTokens,
|
|
5926
6136
|
totalCredits: (currentSession.metadata.totalCredits || 0) + (result.completionInfo.totalCredits || 0),
|
|
5927
|
-
toolCallCount: currentSession.metadata.toolCallCount + successfulToolCalls
|
|
6137
|
+
toolCallCount: currentSession.metadata.toolCallCount + successfulToolCalls,
|
|
6138
|
+
workflow: decisionStoreRef.current.decisions.length > 0 || blockerStoreRef.current.blockers.length > 0 ? {
|
|
6139
|
+
decisions: decisionStoreRef.current.decisions,
|
|
6140
|
+
blockers: blockerStoreRef.current.blockers,
|
|
6141
|
+
handoff: currentSession.metadata.workflow?.handoff,
|
|
6142
|
+
reviewGates: currentSession.metadata.workflow?.reviewGates
|
|
6143
|
+
} : currentSession.metadata.workflow
|
|
5928
6144
|
}
|
|
5929
6145
|
};
|
|
5930
6146
|
setState((prev) => ({
|
|
@@ -6407,6 +6623,46 @@ function CliApp() {
|
|
|
6407
6623
|
console.log("");
|
|
6408
6624
|
}
|
|
6409
6625
|
};
|
|
6626
|
+
/**
|
|
6627
|
+
* Generate a structured session handoff via a single LLM call and persist it
|
|
6628
|
+
* onto the session's workflow state. Returns the handoff on success, or null
|
|
6629
|
+
* if generation was skipped (short session) or failed (parse / agent error).
|
|
6630
|
+
*
|
|
6631
|
+
* Failures are best-effort and surfaced as a warning rather than thrown —
|
|
6632
|
+
* the surrounding /save flow must not block on handoff generation.
|
|
6633
|
+
*
|
|
6634
|
+
* Callers are responsible for saving the session afterwards.
|
|
6635
|
+
*/
|
|
6636
|
+
const generateHandoff = async (session) => {
|
|
6637
|
+
if (!state.agent) return null;
|
|
6638
|
+
const prompt = buildHandoffPrompt(session);
|
|
6639
|
+
if (!prompt) return null;
|
|
6640
|
+
console.log("📝 Generating session handoff...");
|
|
6641
|
+
useCliStore.getState().setIsThinking(true);
|
|
6642
|
+
try {
|
|
6643
|
+
const result = await state.agent.run(prompt, { maxIterations: 1 });
|
|
6644
|
+
const handoff = parseHandoffResponse(result.finalAnswer);
|
|
6645
|
+
if (!handoff) {
|
|
6646
|
+
console.warn("⚠️ Handoff generation returned no parseable JSON; skipping.");
|
|
6647
|
+
logger.debug(`Handoff response: ${result.finalAnswer.slice(0, 500)}`);
|
|
6648
|
+
return null;
|
|
6649
|
+
}
|
|
6650
|
+
session.metadata.workflow = {
|
|
6651
|
+
decisions: decisionStoreRef.current.decisions,
|
|
6652
|
+
blockers: blockerStoreRef.current.blockers,
|
|
6653
|
+
handoff,
|
|
6654
|
+
reviewGates: session.metadata.workflow?.reviewGates
|
|
6655
|
+
};
|
|
6656
|
+
return handoff;
|
|
6657
|
+
} catch (err) {
|
|
6658
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
6659
|
+
console.warn(`⚠️ Handoff generation failed: ${reason}`);
|
|
6660
|
+
logger.debug(`Handoff generation error: ${reason}`);
|
|
6661
|
+
return null;
|
|
6662
|
+
} finally {
|
|
6663
|
+
useCliStore.getState().setIsThinking(false);
|
|
6664
|
+
}
|
|
6665
|
+
};
|
|
6410
6666
|
const handleCommand = async (command, args) => {
|
|
6411
6667
|
const customCommand = state.customCommandStore.getCommand(command);
|
|
6412
6668
|
if (customCommand) try {
|
|
@@ -6540,8 +6796,16 @@ Multi-line Input:
|
|
|
6540
6796
|
}
|
|
6541
6797
|
const sessionName = args.join(" ") || state.session.name;
|
|
6542
6798
|
state.session.name = sessionName;
|
|
6799
|
+
if (decisionStoreRef.current.decisions.length > 0 || blockerStoreRef.current.blockers.length > 0) state.session.metadata.workflow = {
|
|
6800
|
+
decisions: decisionStoreRef.current.decisions,
|
|
6801
|
+
blockers: blockerStoreRef.current.blockers,
|
|
6802
|
+
handoff: state.session.metadata.workflow?.handoff,
|
|
6803
|
+
reviewGates: state.session.metadata.workflow?.reviewGates
|
|
6804
|
+
};
|
|
6805
|
+
const handoff = await generateHandoff(state.session);
|
|
6543
6806
|
await state.sessionStore.save(state.session);
|
|
6544
6807
|
console.log(`✅ Session saved as "${sessionName}"`);
|
|
6808
|
+
if (handoff) console.log("🤝 Session handoff generated");
|
|
6545
6809
|
break;
|
|
6546
6810
|
}
|
|
6547
6811
|
case "resume":
|
|
@@ -6567,15 +6831,24 @@ Multi-line Input:
|
|
|
6567
6831
|
await logger.initialize(loadedSession.id);
|
|
6568
6832
|
logger.debug("=== Session Resumed ===");
|
|
6569
6833
|
if (state.checkpointStore) state.checkpointStore.setSessionId(loadedSession.id);
|
|
6834
|
+
const handoff = loadedSession.metadata.workflow?.handoff;
|
|
6835
|
+
const sessionForState = handoff ? {
|
|
6836
|
+
...loadedSession,
|
|
6837
|
+
messages: injectHandoffMessage(loadedSession.messages, handoff)
|
|
6838
|
+
} : loadedSession;
|
|
6570
6839
|
setState((prev) => ({
|
|
6571
6840
|
...prev,
|
|
6572
|
-
session:
|
|
6841
|
+
session: sessionForState
|
|
6573
6842
|
}));
|
|
6574
|
-
setStoreSession(
|
|
6843
|
+
setStoreSession(sessionForState);
|
|
6575
6844
|
useCliStore.getState().clearPendingMessages();
|
|
6576
6845
|
usageCache = null;
|
|
6577
|
-
console.log(`\n✅ Session resumed: "${
|
|
6578
|
-
console.log(`📝 ${
|
|
6846
|
+
console.log(`\n✅ Session resumed: "${sessionForState.name}"`);
|
|
6847
|
+
console.log(`📝 ${sessionForState.messages.length} messages | 🤖 ${sessionForState.model} | 📊 ${sessionForState.metadata.totalTokens.toLocaleString()} tokens\n`);
|
|
6848
|
+
if (handoff) {
|
|
6849
|
+
console.log("🤝 Session handoff:\n");
|
|
6850
|
+
console.log(formatHandoffOutput(handoff));
|
|
6851
|
+
}
|
|
6579
6852
|
};
|
|
6580
6853
|
setState((prev) => ({
|
|
6581
6854
|
...prev,
|
|
@@ -6800,6 +7073,8 @@ Multi-line Input:
|
|
|
6800
7073
|
};
|
|
6801
7074
|
await logger.initialize(newSession.id);
|
|
6802
7075
|
logger.debug("=== New Session Started via /clear ===");
|
|
7076
|
+
decisionStoreRef.current.decisions = [];
|
|
7077
|
+
blockerStoreRef.current.blockers = [];
|
|
6803
7078
|
if (state.checkpointStore) state.checkpointStore.setSessionId(newSession.id);
|
|
6804
7079
|
setState((prev) => ({
|
|
6805
7080
|
...prev,
|
|
@@ -7491,6 +7766,48 @@ Multi-line Input:
|
|
|
7491
7766
|
console.log(`✅ Removed directory: ${resolvedRemovePath}`);
|
|
7492
7767
|
break;
|
|
7493
7768
|
}
|
|
7769
|
+
case "decisions":
|
|
7770
|
+
console.log("\n📋 Decision Log\n");
|
|
7771
|
+
console.log(formatDecisionsOutput(decisionStoreRef.current.decisions));
|
|
7772
|
+
console.log("");
|
|
7773
|
+
break;
|
|
7774
|
+
case "blockers":
|
|
7775
|
+
console.log("\n🚧 Blockers\n");
|
|
7776
|
+
console.log(formatBlockersOutput(blockerStoreRef.current.blockers));
|
|
7777
|
+
console.log("");
|
|
7778
|
+
break;
|
|
7779
|
+
case "handoff": {
|
|
7780
|
+
if (!state.session) {
|
|
7781
|
+
console.log("No active session");
|
|
7782
|
+
break;
|
|
7783
|
+
}
|
|
7784
|
+
const existing = state.session.metadata.workflow?.handoff;
|
|
7785
|
+
const wantsRegen = args[0] === "generate" || args[0] === "regen";
|
|
7786
|
+
if (existing && !wantsRegen) {
|
|
7787
|
+
console.log("\n🤝 Session handoff\n");
|
|
7788
|
+
console.log(formatHandoffOutput(existing));
|
|
7789
|
+
console.log("Run /handoff generate to refresh.\n");
|
|
7790
|
+
break;
|
|
7791
|
+
}
|
|
7792
|
+
if (state.session.messages.length < 4) {
|
|
7793
|
+
console.log(`Not enough messages to generate a handoff (need at least 4)`);
|
|
7794
|
+
break;
|
|
7795
|
+
}
|
|
7796
|
+
if (!state.agent) {
|
|
7797
|
+
console.log("Cannot generate handoff: no active agent");
|
|
7798
|
+
break;
|
|
7799
|
+
}
|
|
7800
|
+
const handoff = await generateHandoff(state.session);
|
|
7801
|
+
if (!handoff) {
|
|
7802
|
+
console.log("❌ Failed to generate handoff");
|
|
7803
|
+
break;
|
|
7804
|
+
}
|
|
7805
|
+
await state.sessionStore.save(state.session);
|
|
7806
|
+
console.log("\n🤝 Session handoff\n");
|
|
7807
|
+
console.log(formatHandoffOutput(handoff));
|
|
7808
|
+
console.log("\n✅ Session saved with refreshed handoff");
|
|
7809
|
+
break;
|
|
7810
|
+
}
|
|
7494
7811
|
case "dirs": {
|
|
7495
7812
|
const additionalDirs = await state.configStore.getAdditionalDirectories();
|
|
7496
7813
|
const cwd = process.cwd();
|