@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.
@@ -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(8e3),
1073
+ maxOutputTokens: z.number().default(16e3),
1066
1074
  maxGithubApiCalls: z.number().default(20)
1067
1075
  }).default({
1068
1076
  maxInputTokens: 5e4,
1069
- maxOutputTokens: 8e3,
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-DcJC1C8S.mjs";
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 { B as ReActAgent, C as loadContextFiles, D as generateCliTools, E as PermissionManager, F as setWebSocketToolExecutor, H as CheckpointStore, L as buildCoreSystemPrompt, V as CustomCommandStore, W as SessionStore, _ as WebSocketLlmBackend, a as createCoordinateTaskTool, c as createAgentDelegateTool, d as createSkillTool, g as FallbackLlmBackend, h as WebSocketConnectionManager, i as createWriteTodosTool, l as AgentStore, m as WebSocketToolExecutor, n as createFindDefinitionTool, o as createBackgroundAgentTools, p as ApiClient, r as createTodoStore, s as BackgroundAgentManager, t as createGetFileStructureTool, u as SubagentOrchestrator, v as ServerLlmBackend, w as getApiUrl, y as McpManager, z as isReadOnlyTool } from "../tools-DiSJh1Dv.mjs";
3
- import { n as logger, t as ConfigStore } from "../ConfigStore-DBUmvCfe.mjs";
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 { t as ConfigStore } from "../ConfigStore-DBUmvCfe.mjs";
2
+ import { t as ConfigStore } from "../ConfigStore-Dt6utdSA.mjs";
3
3
  //#region src/commands/mcpCommand.ts
4
4
  /**
5
5
  * External MCP commands (b4m mcp list, b4m mcp add, etc.)
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { i as version, r as forceCheckForUpdate } from "../updateChecker-DcJC1C8S.mjs";
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 DEFAULT_MAX_ITERATIONS, B as ReActAgent, C as loadContextFiles, D as generateCliTools, E as PermissionManager, F as setWebSocketToolExecutor, G as OAuthClient, H as CheckpointStore, I as OllamaBackend, J as searchCommands, K as hasFileReferences, L as buildCoreSystemPrompt, M as DEFAULT_THOROUGHNESS, N as clearFeatureModuleTools, O as ALWAYS_DENIED_FOR_AGENTS, P as registerFeatureModuleTools, Q as warmFileCache, R as buildSkillsPromptSection, S as extractCompactInstructions, T as getEnvironmentName, U as CommandHistoryStore, V as CustomCommandStore, W as SessionStore, X as formatFileSize, Y as mergeCommands, Z as searchFiles, _ as WebSocketLlmBackend, a as createCoordinateTaskTool, b as substituteArguments, c as createAgentDelegateTool, d as createSkillTool, f as parseAgentConfig, g as FallbackLlmBackend, h as WebSocketConnectionManager, i as createWriteTodosTool, j as DEFAULT_RETRY_CONFIG, k as DEFAULT_AGENT_MODEL, l as AgentStore, m as WebSocketToolExecutor, n as createFindDefinitionTool, o as createBackgroundAgentTools, p as ApiClient, q as processFileReferences, r as createTodoStore, s as BackgroundAgentManager, t as createGetFileStructureTool, u as SubagentOrchestrator, v as ServerLlmBackend, w as getApiUrl, x as formatStep, y as McpManager, z as isReadOnlyTool } from "./tools-DiSJh1Dv.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-DBUmvCfe.mjs";
5
- import { i as version, t as checkForUpdate } from "./updateChecker-DcJC1C8S.mjs";
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: loadedSession
6841
+ session: sessionForState
6573
6842
  }));
6574
- setStoreSession(loadedSession);
6843
+ setStoreSession(sessionForState);
6575
6844
  useCliStore.getState().clearPendingMessages();
6576
6845
  usageCache = null;
6577
- console.log(`\n✅ Session resumed: "${loadedSession.name}"`);
6578
- console.log(`📝 ${loadedSession.messages.length} messages | 🤖 ${loadedSession.model} | 📊 ${loadedSession.metadata.totalTokens.toLocaleString()} tokens\n`);
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();