whipped 0.8.1 → 0.9.1

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.
Files changed (86) hide show
  1. package/dist/cli.js +1296 -311
  2. package/dist/mcp-server.js +150 -2
  3. package/dist/migrations/014_companion_sessions.sql +34 -0
  4. package/dist/migrations/015_companion_worktree_mode.sql +45 -0
  5. package/dist/migrations/016_companion_plans.sql +22 -0
  6. package/dist/migrations/017_companion_saved_plans.sql +23 -0
  7. package/dist/migrations/018_generalize_plan_session_ref.sql +29 -0
  8. package/dist/migrations/019_rename_plan_to_canvas.sql +20 -0
  9. package/dist/web-ui/assets/abnfDiagram-VRR7QNED-CMDqE9Fd.js +119 -0
  10. package/dist/web-ui/assets/arc-Dbpy-AQ8.js +131 -0
  11. package/dist/web-ui/assets/architectureDiagram-ZJ3FMSHR-BKEqdC5c.js +8821 -0
  12. package/dist/web-ui/assets/blockDiagram-677ZJIJ3-CTsEohS4.js +3801 -0
  13. package/dist/web-ui/assets/c4Diagram-LMCZKHZV-Dn6mvxwq.js +2479 -0
  14. package/dist/web-ui/assets/channel-CGIzRjBJ.js +7 -0
  15. package/dist/web-ui/assets/chunk-2Q5K7J3B-DYVO9tIE.js +17 -0
  16. package/dist/web-ui/assets/chunk-32BRIVSS-CqBj7LwD.js +116 -0
  17. package/dist/web-ui/assets/chunk-5VM5RSS4-D6as2qUK.js +19 -0
  18. package/dist/web-ui/assets/chunk-EX3LRPZG-Daf_kmKB.js +1996 -0
  19. package/dist/web-ui/assets/chunk-JWPE2WC7-Ab-zf2K1.js +17 -0
  20. package/dist/web-ui/assets/chunk-MOJQB5TN-DnYfTlwW.js +855 -0
  21. package/dist/web-ui/assets/chunk-RYQCIY6F-BkiNDZ_4.js +476 -0
  22. package/dist/web-ui/assets/chunk-V7JOEXUC-DCb_30mT.js +2022 -0
  23. package/dist/web-ui/assets/chunk-VR4S4FIN-CO86AkST.js +25 -0
  24. package/dist/web-ui/assets/chunk-XXDRQBXY--g2YuB3U.js +13 -0
  25. package/dist/web-ui/assets/classDiagram-OUVF2IWQ-C25Jck2H.js +24 -0
  26. package/dist/web-ui/assets/classDiagram-v2-EOCWNBFH-C25Jck2H.js +24 -0
  27. package/dist/web-ui/assets/cose-bilkent-JH36ORCC-DhhXza2J.js +4943 -0
  28. package/dist/web-ui/assets/cynefin-VYW2F7L2-CaR1DgV9.js +31527 -0
  29. package/dist/web-ui/assets/cynefinDiagram-TSTJHNR4-CLGnTJf1.js +454 -0
  30. package/dist/web-ui/assets/cytoscape.esm-CaQ7Fomf.js +30346 -0
  31. package/dist/web-ui/assets/dagre-VKFMJZFB-DitV5zjf.js +526 -0
  32. package/dist/web-ui/assets/defaultLocale-B2RvLBDe.js +206 -0
  33. package/dist/web-ui/assets/diagram-FQU43EPY-C1rGhyyl.js +636 -0
  34. package/dist/web-ui/assets/diagram-G47NLZAW-CTQhjAQX.js +858 -0
  35. package/dist/web-ui/assets/diagram-NH7WQ7WH-DI8N7xbl.js +212 -0
  36. package/dist/web-ui/assets/diagram-OA4YK3LP-WcmQFHPD.js +492 -0
  37. package/dist/web-ui/assets/diagram-WEI45ONY-P5J7jo04.js +309 -0
  38. package/dist/web-ui/assets/ebnfDiagram-CCIWWBDH-VgG6WhIs.js +139 -0
  39. package/dist/web-ui/assets/erDiagram-Q63AITRT-aQJn3J15.js +1238 -0
  40. package/dist/web-ui/assets/flowDiagram-23GEKE2U-mdFXB92B.js +2353 -0
  41. package/dist/web-ui/assets/ganttDiagram-NO4QXBWP-CqDgijmY.js +3733 -0
  42. package/dist/web-ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  43. package/dist/web-ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  44. package/dist/web-ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  45. package/dist/web-ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  46. package/dist/web-ui/assets/geist-mono-cyrillic-ext-wght-normal-I4S5GZfc.woff2 +0 -0
  47. package/dist/web-ui/assets/geist-mono-cyrillic-wght-normal-BmXc_FBt.woff2 +0 -0
  48. package/dist/web-ui/assets/geist-mono-latin-ext-wght-normal-DrnZ1wKl.woff2 +0 -0
  49. package/dist/web-ui/assets/geist-mono-latin-wght-normal-B_7UjwxQ.woff2 +0 -0
  50. package/dist/web-ui/assets/geist-mono-symbols2-wght-normal-GZpp1pK2.woff2 +0 -0
  51. package/dist/web-ui/assets/geist-mono-vietnamese-wght-normal-D8KDMBhC.woff2 +0 -0
  52. package/dist/web-ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  53. package/dist/web-ui/assets/gitGraphDiagram-IHSO6WYX-8juiaoTk.js +1385 -0
  54. package/dist/web-ui/assets/graph-BMLV0goG.js +2042 -0
  55. package/dist/web-ui/assets/{index-CRXPsGTP.css → index-DPjATOCj.css} +800 -1207
  56. package/dist/web-ui/assets/{index-BMFVAmy4.js → index-DddtVpjm.js} +41479 -39640
  57. package/dist/web-ui/assets/infoDiagram-FWYZ7A6U-C2g8E3ea.js +32 -0
  58. package/dist/web-ui/assets/init-ZxktEp_H.js +16 -0
  59. package/dist/web-ui/assets/ishikawaDiagram-FXEZZL3T-cBRjKZAE.js +967 -0
  60. package/dist/web-ui/assets/journeyDiagram-5HDEW3XC-BvGisWzY.js +1256 -0
  61. package/dist/web-ui/assets/kanban-definition-HUTT4EX6-xCU5FVAS.js +1055 -0
  62. package/dist/web-ui/assets/katex-CqNtglxf.js +14499 -0
  63. package/dist/web-ui/assets/layout-BNmRhaUB.js +2359 -0
  64. package/dist/web-ui/assets/linear-uVfTbk22.js +340 -0
  65. package/dist/web-ui/assets/map-BEO0Bu8q.js +298 -0
  66. package/dist/web-ui/assets/mermaid.core-D-SdXkuv.js +26639 -0
  67. package/dist/web-ui/assets/mindmap-definition-LN4V7U3C-BXMrLpcc.js +1183 -0
  68. package/dist/web-ui/assets/ordinal-DSZU4PqD.js +76 -0
  69. package/dist/web-ui/assets/pegDiagram-2B236MQR-4QY6zfTY.js +127 -0
  70. package/dist/web-ui/assets/pieDiagram-ENE6RG2P-CvA8hnwZ.js +318 -0
  71. package/dist/web-ui/assets/quadrantDiagram-ABIIQ3AL-b9LyRoDu.js +1341 -0
  72. package/dist/web-ui/assets/railroadDiagram-RFXS5EU6-xnbYx8zt.js +93 -0
  73. package/dist/web-ui/assets/requirementDiagram-TGXJPOKE-DXgeFZvD.js +1205 -0
  74. package/dist/web-ui/assets/sankeyDiagram-HTMAVEWB-N3WPRpVR.js +1264 -0
  75. package/dist/web-ui/assets/sequenceDiagram-DBY2YBRQ-CasLOrw_.js +4523 -0
  76. package/dist/web-ui/assets/sizeCapture-X5ZJPWSS-DlBvxVbP.js +64 -0
  77. package/dist/web-ui/assets/stateDiagram-2N3HPSRC-fp4Rfa7y.js +453 -0
  78. package/dist/web-ui/assets/stateDiagram-v2-6OUMAXLB-B1Sbo4u9.js +23 -0
  79. package/dist/web-ui/assets/swimlanes-5IMT3BWC-D8woP0NL.js +8575 -0
  80. package/dist/web-ui/assets/swimlanesDiagram-G3AALYLV-BkAvTJ1E.js +21 -0
  81. package/dist/web-ui/assets/timeline-definition-FHXFAJF6-Bri3dfoP.js +1606 -0
  82. package/dist/web-ui/assets/vennDiagram-L72KCM5P-DTZlIjiw.js +2523 -0
  83. package/dist/web-ui/assets/wardleyDiagram-EHGQE667-CCyt_RTI.js +978 -0
  84. package/dist/web-ui/assets/xychartDiagram-FW5EYKEG-DtQR47sr.js +1972 -0
  85. package/dist/web-ui/index.html +2 -2
  86. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -3529,7 +3529,7 @@ function isResumableSessionState(state) {
3529
3529
  function normalizeTag(raw2) {
3530
3530
  return raw2.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3531
3531
  }
3532
- var ASSISTANT_AGENT_PREFIX, runtimeAgentIdSchema, effortLevelSchema, agentModelChoiceSchema, workflowSlotTypeSchema, tierLevelSchema, LEVEL_ORDER, modelPairSchema, pairSelectionModeSchema, SLOT_TOOL_IDS, slotToolSchema, slotModelConfigSchema, cardModelConfigSchema, promptValueSchema, EMPTY_INLINE_PROMPT, workflowSlotSchema, DEFAULT_MODEL_PAIR, DEFAULT_SLOT_MODEL_FIELDS, workflowSchema, DEFAULT_WORKFLOW, DEFAULT_STORY_WORKFLOW, DEFAULT_GIT_INSTRUCTIONS, runtimeBoardColumnIdSchema, BOARD_COLUMNS, reviewActorSchema, reviewIssueSchema, reviewAttachmentSchema, runtimeReviewCommentSchema, runtimeActivityEntrySchema, runtimeTaskSessionStateSchema, runtimeTerminalSessionEntrySchema, runtimeCardPrioritySchema, cardTypeSchema, runtimePrMetaSchema, runtimeBoardCardSchema, runtimeBoardColumnSchema, runtimeBoardDataSchema, notificationSoundsConfigSchema, runtimeGlobalConfigSchema, runtimeGithubConfigSchema, runtimeWorktreeCopyEntrySchema, runtimeWorktreeSetupSchema, runtimeProjectSecretSchema, runtimeProjectConfigSchema, runtimeWorkspaceStateResponseSchema, runtimeWorkspaceStateSaveRequestSchema, runtimeVisualElementSchema, runtimeVisualCommentSchema, runtimeCardCreateRequestSchema, runtimeBulkCardImportItemSchema, runtimeBulkCardsCreateRequestSchema, runtimeCardMoveRequestSchema, runtimeCardUpdateRequestSchema, memoryScopeSchema, memoryTypeSchema, memorySourceTypeSchema, memoryStatusSchema, runtimeMemoryOriginAgentSchema, runtimeMemorySchema, recurringScheduleKindSchema, recurringScheduleSchema, recurringRunStatusSchema, recurringRunTriggerSchema, recurringAgentRunSchema, recurringAgentSchema, recurringAgentCreateRequestSchema, recurringAgentUpdateRequestSchema, projectFolderSchema, topLevelItemSchema, projectsLayoutSchema, runtimeProjectSchema;
3532
+ var ASSISTANT_AGENT_PREFIX, runtimeAgentIdSchema, effortLevelSchema, agentModelChoiceSchema, DEFAULT_AGENT_MODEL_CHOICE, workflowSlotTypeSchema, tierLevelSchema, LEVEL_ORDER, modelPairSchema, pairSelectionModeSchema, SLOT_TOOL_IDS, slotToolSchema, slotModelConfigSchema, cardModelConfigSchema, promptValueSchema, EMPTY_INLINE_PROMPT, workflowSlotSchema, DEFAULT_MODEL_PAIR, DEFAULT_SLOT_MODEL_FIELDS, workflowSchema, DEFAULT_WORKFLOW, DEFAULT_STORY_WORKFLOW, DEFAULT_GIT_INSTRUCTIONS, runtimeBoardColumnIdSchema, BOARD_COLUMNS, reviewActorSchema, reviewIssueSchema, reviewAttachmentSchema, runtimeReviewCommentSchema, runtimeActivityEntrySchema, runtimeTaskSessionStateSchema, runtimeTerminalSessionEntrySchema, runtimeCardPrioritySchema, cardTypeSchema, runtimePrMetaSchema, runtimeBoardCardSchema, runtimeBoardColumnSchema, runtimeBoardDataSchema, notificationSoundsConfigSchema, runtimeGlobalConfigSchema, runtimeGithubConfigSchema, runtimeWorktreeCopyEntrySchema, runtimeWorktreeSetupSchema, runtimeProjectSecretSchema, runtimeProjectConfigSchema, runtimeWorkspaceStateResponseSchema, runtimeWorkspaceStateSaveRequestSchema, runtimeVisualElementSchema, runtimeVisualCommentSchema, runtimeCardCreateRequestSchema, runtimeBulkCardImportItemSchema, runtimeBulkCardsCreateRequestSchema, runtimeCardMoveRequestSchema, runtimeCardUpdateRequestSchema, memoryScopeSchema, memoryTypeSchema, memorySourceTypeSchema, memoryStatusSchema, runtimeMemoryOriginAgentSchema, runtimeMemorySchema, recurringScheduleKindSchema, recurringScheduleSchema, recurringRunStatusSchema, recurringRunTriggerSchema, recurringAgentRunSchema, recurringAgentSchema, recurringAgentCreateRequestSchema, recurringAgentUpdateRequestSchema, companionSessionStatusSchema, companionSessionSchema, companionSessionCreateRequestSchema, choiceOptionSchema, REQUIRED_FIELD_DESCRIPTION, questionLeafInputSchema, questionInputSchema, canvasBlockSchema, canvasDocumentSchema, companionSavedCanvasSchema, projectFolderSchema, topLevelItemSchema, projectsLayoutSchema, runtimeProjectSchema;
3533
3533
  var init_api_contract = __esm({
3534
3534
  "src/core/api-contract.ts"() {
3535
3535
  "use strict";
@@ -3541,6 +3541,7 @@ var init_api_contract = __esm({
3541
3541
  model: z.string().nullable().optional(),
3542
3542
  effort: effortLevelSchema.nullable().optional()
3543
3543
  });
3544
+ DEFAULT_AGENT_MODEL_CHOICE = { agentId: "claude", model: null, effort: null };
3544
3545
  workflowSlotTypeSchema = z.enum(["dev", "review", "plan", "orch"]);
3545
3546
  tierLevelSchema = z.enum(["minimal", "low", "medium", "high", "max"]);
3546
3547
  LEVEL_ORDER = ["minimal", "low", "medium", "high", "max"];
@@ -4090,6 +4091,104 @@ Do NOT include:
4090
4091
  enabled: z.boolean().optional(),
4091
4092
  journal: z.string().optional()
4092
4093
  });
4094
+ companionSessionStatusSchema = z.enum(["installing", "running", "stopped", "merged", "discarded"]);
4095
+ companionSessionSchema = z.object({
4096
+ id: z.string(),
4097
+ name: z.string(),
4098
+ useWorktree: z.boolean(),
4099
+ baseRef: z.string(),
4100
+ branchName: z.string().nullable(),
4101
+ worktreePath: z.string().nullable(),
4102
+ workflowId: z.string().nullable(),
4103
+ seedPrompt: z.string().default(""),
4104
+ agentId: runtimeAgentIdSchema,
4105
+ model: z.string().nullable(),
4106
+ effort: effortLevelSchema.nullable(),
4107
+ status: companionSessionStatusSchema.default("stopped"),
4108
+ savedCanvasId: z.string().nullable(),
4109
+ createdAt: z.number(),
4110
+ updatedAt: z.number()
4111
+ });
4112
+ companionSessionCreateRequestSchema = z.object({
4113
+ name: z.string().optional(),
4114
+ useWorktree: z.boolean().default(true),
4115
+ baseRef: z.string().min(1),
4116
+ branchName: z.string().optional(),
4117
+ workflowId: z.string().optional(),
4118
+ model: agentModelChoiceSchema.optional(),
4119
+ savedCanvasId: z.string().optional()
4120
+ });
4121
+ choiceOptionSchema = z.object({
4122
+ value: z.string(),
4123
+ label: z.string(),
4124
+ description: z.string().optional()
4125
+ });
4126
+ REQUIRED_FIELD_DESCRIPTION = 'Signal only \u2014 not enforced by the panel, the developer can send/approve without answering. If this comes back "(not answered)" and you still need it, ask again in your next canvas version.';
4127
+ questionLeafInputSchema = z.discriminatedUnion("kind", [
4128
+ z.object({
4129
+ kind: z.literal("single_choice"),
4130
+ name: z.string(),
4131
+ label: z.string().optional(),
4132
+ options: z.array(choiceOptionSchema),
4133
+ allowOther: z.boolean().optional(),
4134
+ required: z.boolean().optional().describe(REQUIRED_FIELD_DESCRIPTION)
4135
+ }),
4136
+ z.object({
4137
+ kind: z.literal("multi_choice"),
4138
+ name: z.string(),
4139
+ label: z.string().optional(),
4140
+ options: z.array(choiceOptionSchema),
4141
+ allowOther: z.boolean().optional(),
4142
+ required: z.boolean().optional().describe(REQUIRED_FIELD_DESCRIPTION)
4143
+ }),
4144
+ z.object({
4145
+ kind: z.literal("text"),
4146
+ name: z.string(),
4147
+ label: z.string().optional(),
4148
+ placeholder: z.string().optional(),
4149
+ multiline: z.boolean().optional(),
4150
+ required: z.boolean().optional().describe(REQUIRED_FIELD_DESCRIPTION)
4151
+ })
4152
+ ]);
4153
+ questionInputSchema = z.discriminatedUnion("kind", [
4154
+ ...questionLeafInputSchema.options,
4155
+ z.object({ kind: z.literal("composite"), parts: z.array(questionLeafInputSchema) })
4156
+ ]);
4157
+ canvasBlockSchema = z.discriminatedUnion("type", [
4158
+ z.object({ id: z.string(), type: z.literal("markdown"), body: z.string() }),
4159
+ // Rendered via dangerouslySetInnerHTML, unsanitized — same trust boundary as
4160
+ // the markdown block's rehype-raw pass-through (the agent is the user's own
4161
+ // coding agent, not untrusted third-party input).
4162
+ z.object({
4163
+ id: z.string(),
4164
+ type: z.literal("html"),
4165
+ body: z.string().describe(
4166
+ `Raw HTML rendered via dangerouslySetInnerHTML at runtime. Use this whenever the developer wants to see UI, layout, or visual design \u2014 a dashboard, a page structure, a component arrangement \u2014 since markdown can only describe that in prose while an actual html mockup shows it. It is NOT run through the app's build-time Tailwind compiler, so Tailwind utility classes (e.g. class="grid grid-cols-3 gap-4") produce no CSS and render unstyled \u2014 style the mockup with inline style="..." attributes, or a <style> block scoped to unique ids/classes you define within the same body.`
4167
+ )
4168
+ }),
4169
+ z.object({
4170
+ id: z.string(),
4171
+ type: z.literal("diagram"),
4172
+ format: z.literal("mermaid"),
4173
+ source: z.string(),
4174
+ caption: z.string().optional()
4175
+ }),
4176
+ z.object({ id: z.string(), type: z.literal("question"), prompt: z.string(), input: questionInputSchema })
4177
+ ]);
4178
+ canvasDocumentSchema = z.object({
4179
+ version: z.number(),
4180
+ createdAt: z.number(),
4181
+ blocks: z.array(canvasBlockSchema)
4182
+ });
4183
+ companionSavedCanvasSchema = z.object({
4184
+ id: z.string(),
4185
+ workspaceId: z.string(),
4186
+ title: z.string(),
4187
+ blocks: z.array(canvasBlockSchema),
4188
+ sourceSessionId: z.string().nullable(),
4189
+ createdAt: z.number(),
4190
+ updatedAt: z.number()
4191
+ });
4093
4192
  projectFolderSchema = z.object({
4094
4193
  id: z.string(),
4095
4194
  name: z.string(),
@@ -13623,9 +13722,9 @@ function installGracefulShutdownHandlers(options) {
13623
13722
  init_logger();
13624
13723
 
13625
13724
  // src/server/runtime-server.ts
13626
- import { existsSync as existsSync14, readFileSync as readFileSync8 } from "node:fs";
13725
+ import { existsSync as existsSync15, readFileSync as readFileSync8 } from "node:fs";
13627
13726
  import { createServer } from "node:http";
13628
- import { join as join22 } from "node:path";
13727
+ import { join as join23 } from "node:path";
13629
13728
  import { fileURLToPath as fileURLToPath5 } from "node:url";
13630
13729
  import * as nodePty3 from "node-pty";
13631
13730
 
@@ -13670,6 +13769,7 @@ var HOOKS_DIR = join9(WHIPPED_HOME_DIR, "hooks");
13670
13769
  var CLAUDE_TASK_SETTINGS_PATH = join9(HOOKS_DIR, "claude-task-settings.json");
13671
13770
  var CLAUDE_ASSISTANT_MCP_CONFIG_PATH = join9(HOOKS_DIR, "claude-assistant-mcp-config.json");
13672
13771
  var CLAUDE_REVIEW_MCP_CONFIG_PATH = join9(HOOKS_DIR, "claude-review-mcp-config.json");
13772
+ var CLAUDE_COMPANION_SETTINGS_PATH = join9(HOOKS_DIR, "claude-companion-settings.json");
13673
13773
  var HOOK_TASK_ID_ENV = "WHIPPED_HOOK_TASK_ID";
13674
13774
  var HOOK_WORKSPACE_ID_ENV = "WHIPPED_HOOK_WORKSPACE_ID";
13675
13775
  function getServerPort(serverUrl) {
@@ -13696,10 +13796,18 @@ async function writeClaudeTaskHookSettings(serverPort) {
13696
13796
  await mkdir(HOOKS_DIR, { recursive: true });
13697
13797
  await writeFile(CLAUDE_TASK_SETTINGS_PATH, JSON.stringify(settings, null, 2));
13698
13798
  }
13699
- function buildMcpRoleArgs(role, recurringAgentId) {
13799
+ async function writeClaudeCompanionSettings() {
13800
+ const settings = {
13801
+ permissions: { deny: ["EnterPlanMode", "ExitPlanMode"] }
13802
+ };
13803
+ await mkdir(HOOKS_DIR, { recursive: true });
13804
+ await writeFile(CLAUDE_COMPANION_SETTINGS_PATH, JSON.stringify(settings, null, 2));
13805
+ }
13806
+ function buildMcpRoleArgs(role, recurringAgentId, companionSessionId) {
13700
13807
  const args = [];
13701
13808
  if (role) args.push(`--role=${role}`);
13702
13809
  if (recurringAgentId) args.push(`--recurring-agent-id=${recurringAgentId}`);
13810
+ if (companionSessionId) args.push(`--companion-session-id=${companionSessionId}`);
13703
13811
  return args;
13704
13812
  }
13705
13813
  function buildWhippedMcpServerSpec(mcp, serverUrl, workspaceId, agentId, extraArgs = []) {
@@ -20484,7 +20592,7 @@ init_workspace_state();
20484
20592
  var import_tree_kill2 = __toESM(require_tree_kill(), 1);
20485
20593
  import { existsSync as existsSync10 } from "node:fs";
20486
20594
  import { cp, link, mkdir as mkdir3, stat as stat2, symlink, unlink as unlink2 } from "node:fs/promises";
20487
- import { dirname as dirname6, join as join17, resolve as resolve2 } from "node:path";
20595
+ import { dirname as dirname6, join as join18, resolve as resolve2 } from "node:path";
20488
20596
  import { fileURLToPath as fileURLToPath4 } from "node:url";
20489
20597
  import * as nodePty2 from "node-pty";
20490
20598
  init_api_contract();
@@ -20508,13 +20616,320 @@ function resolvePromptText(prompt, repoPath) {
20508
20616
 
20509
20617
  // src/daemon/scheduler.ts
20510
20618
  init_task_id();
20619
+
20620
+ // src/state/companion-canvases-store.ts
20621
+ init_task_id();
20622
+ init_db();
20623
+ var RECENT_CANVASES_LIMIT = 20;
20624
+ function safeJsonParse2(raw2, fallback) {
20625
+ try {
20626
+ return JSON.parse(raw2);
20627
+ } catch {
20628
+ return fallback;
20629
+ }
20630
+ }
20631
+ function canvasFromRow(row) {
20632
+ return {
20633
+ version: row.version,
20634
+ createdAt: row.created_at,
20635
+ blocks: safeJsonParse2(row.blocks_json, [])
20636
+ };
20637
+ }
20638
+ function nextVersion(sessionId) {
20639
+ const row = getDb().prepare("SELECT MAX(version) as max FROM companion_canvases WHERE session_id = ?").get(sessionId);
20640
+ return (row?.max ?? 0) + 1;
20641
+ }
20642
+ function createCompanionCanvas(sessionId, workspaceId, blocks) {
20643
+ const id = `cpv_${generateTaskId()}`;
20644
+ const version = nextVersion(sessionId);
20645
+ const createdAt = Date.now();
20646
+ getDb().prepare(
20647
+ "INSERT INTO companion_canvases (id, session_id, workspace_id, version, blocks_json, created_at) VALUES (?, ?, ?, ?, ?, ?)"
20648
+ ).run(id, sessionId, workspaceId, version, JSON.stringify(blocks), createdAt);
20649
+ return { version, createdAt, blocks };
20650
+ }
20651
+ function listCompanionCanvases(sessionId) {
20652
+ const rows = getDb().prepare("SELECT * FROM companion_canvases WHERE session_id = ? ORDER BY version DESC LIMIT ?").all(sessionId, RECENT_CANVASES_LIMIT);
20653
+ return rows.map(canvasFromRow);
20654
+ }
20655
+ function deleteCompanionCanvasesForSession(sessionId) {
20656
+ getDb().prepare("DELETE FROM companion_canvases WHERE session_id = ?").run(sessionId);
20657
+ }
20658
+
20659
+ // src/state/companion-saved-canvases-store.ts
20660
+ init_task_id();
20661
+ init_db();
20662
+ function safeJsonParse3(raw2, fallback) {
20663
+ try {
20664
+ return JSON.parse(raw2);
20665
+ } catch {
20666
+ return fallback;
20667
+ }
20668
+ }
20669
+ function savedCanvasFromRow(row) {
20670
+ return {
20671
+ id: row.id,
20672
+ workspaceId: row.workspace_id,
20673
+ title: row.title,
20674
+ blocks: safeJsonParse3(row.blocks_json, []),
20675
+ sourceSessionId: row.source_session_id,
20676
+ createdAt: row.created_at,
20677
+ updatedAt: row.updated_at
20678
+ };
20679
+ }
20680
+ function listCompanionSavedCanvases(workspaceId) {
20681
+ const rows = getDb().prepare("SELECT * FROM companion_saved_canvases WHERE workspace_id = ? ORDER BY updated_at DESC").all(workspaceId);
20682
+ return rows.map(savedCanvasFromRow);
20683
+ }
20684
+ function getCompanionSavedCanvas(id) {
20685
+ const row = getDb().prepare("SELECT * FROM companion_saved_canvases WHERE id = ?").get(id);
20686
+ return row ? savedCanvasFromRow(row) : null;
20687
+ }
20688
+ function createCompanionSavedCanvas(workspaceId, input) {
20689
+ const id = `csp_${generateTaskId()}`;
20690
+ const now = Date.now();
20691
+ getDb().prepare(
20692
+ `INSERT INTO companion_saved_canvases
20693
+ (id, workspace_id, title, blocks_json, source_session_id, created_at, updated_at)
20694
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
20695
+ ).run(id, workspaceId, input.title, JSON.stringify(input.blocks), input.sourceSessionId, now, now);
20696
+ const created = getCompanionSavedCanvas(id);
20697
+ if (!created) throw new Error("createCompanionSavedCanvas: row vanished after insert");
20698
+ return created;
20699
+ }
20700
+ function updateCompanionSavedCanvas(id, input) {
20701
+ getDb().prepare("UPDATE companion_saved_canvases SET title = ?, blocks_json = ?, updated_at = ? WHERE id = ?").run(input.title, JSON.stringify(input.blocks), Date.now(), id);
20702
+ return getCompanionSavedCanvas(id);
20703
+ }
20704
+ function deleteCompanionSavedCanvas(id) {
20705
+ getDb().prepare("DELETE FROM companion_saved_canvases WHERE id = ?").run(id);
20706
+ }
20707
+ function findCompanionSavedCanvasBySourceSession(sessionId) {
20708
+ const row = getDb().prepare("SELECT * FROM companion_saved_canvases WHERE source_session_id = ? ORDER BY updated_at DESC LIMIT 1").get(sessionId);
20709
+ return row ? savedCanvasFromRow(row) : null;
20710
+ }
20711
+
20712
+ // src/state/companion-sessions-store.ts
20713
+ init_task_id();
20714
+ init_db();
20715
+ function sessionFromRow(row) {
20716
+ return {
20717
+ id: row.id,
20718
+ name: row.name,
20719
+ useWorktree: row.use_worktree === 1,
20720
+ baseRef: row.base_ref,
20721
+ branchName: row.branch_name,
20722
+ worktreePath: row.worktree_path,
20723
+ workflowId: row.workflow_id,
20724
+ seedPrompt: row.seed_prompt,
20725
+ agentId: row.agent_id,
20726
+ model: row.model,
20727
+ effort: row.effort,
20728
+ status: row.status,
20729
+ savedCanvasId: row.saved_canvas_id,
20730
+ createdAt: row.created_at,
20731
+ updatedAt: row.updated_at
20732
+ };
20733
+ }
20734
+ function listCompanionSessions(workspaceId) {
20735
+ const rows = getDb().prepare("SELECT * FROM companion_sessions WHERE workspace_id = ? ORDER BY created_at DESC").all(workspaceId);
20736
+ return rows.map(sessionFromRow);
20737
+ }
20738
+ function getCompanionSession(id) {
20739
+ const row = getDb().prepare("SELECT * FROM companion_sessions WHERE id = ?").get(id);
20740
+ return row ? sessionFromRow(row) : null;
20741
+ }
20742
+ function createCompanionSession(workspaceId, input) {
20743
+ const id = `cs_${generateTaskId()}`;
20744
+ const now = Date.now();
20745
+ getDb().prepare(
20746
+ `INSERT INTO companion_sessions
20747
+ (id, workspace_id, name, use_worktree, base_ref, branch_name, worktree_path, workflow_id, seed_prompt,
20748
+ agent_id, model, effort, status, saved_canvas_id, created_at, updated_at)
20749
+ VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, 'stopped', ?, ?, ?)`
20750
+ ).run(
20751
+ id,
20752
+ workspaceId,
20753
+ input.name,
20754
+ input.useWorktree ? 1 : 0,
20755
+ input.baseRef,
20756
+ input.branchName,
20757
+ input.workflowId,
20758
+ input.seedPrompt,
20759
+ input.agentId,
20760
+ input.model,
20761
+ input.effort,
20762
+ input.savedCanvasId,
20763
+ now,
20764
+ now
20765
+ );
20766
+ const created = getCompanionSession(id);
20767
+ if (!created) throw new Error("createCompanionSession: row vanished after insert");
20768
+ return created;
20769
+ }
20770
+ function setCompanionSessionSavedCanvasId(id, savedCanvasId) {
20771
+ getDb().prepare("UPDATE companion_sessions SET saved_canvas_id = ?, updated_at = ? WHERE id = ?").run(savedCanvasId, Date.now(), id);
20772
+ }
20773
+ function setCompanionSessionWorktreePath(id, worktreePath) {
20774
+ getDb().prepare("UPDATE companion_sessions SET worktree_path = ?, updated_at = ? WHERE id = ?").run(worktreePath, Date.now(), id);
20775
+ }
20776
+ function setCompanionSessionStatus(id, status) {
20777
+ getDb().prepare("UPDATE companion_sessions SET status = ?, updated_at = ? WHERE id = ?").run(status, Date.now(), id);
20778
+ }
20779
+ function deleteCompanionSession(id) {
20780
+ getDb().prepare("DELETE FROM companion_sessions WHERE id = ?").run(id);
20781
+ }
20782
+ function resetStaleCompanionSessions(workspaceId) {
20783
+ const res = getDb().prepare(
20784
+ "UPDATE companion_sessions SET status = 'stopped', updated_at = ? WHERE workspace_id = ? AND status IN ('running', 'installing')"
20785
+ ).run(Date.now(), workspaceId);
20786
+ return res.changes;
20787
+ }
20788
+
20789
+ // src/daemon/scheduler.ts
20511
20790
  init_workspace_state();
20512
20791
 
20513
- // src/daemon/review-pipeline.ts
20792
+ // src/daemon/companion-agent.ts
20793
+ init_api_contract();
20794
+
20795
+ // src/daemon/canvas-mode-prompt.ts
20796
+ function serializeCanvasBlocksForPrompt(blocks) {
20797
+ return blocks.map((block) => {
20798
+ if (block.type === "markdown") return block.body;
20799
+ if (block.type === "html") return `\`\`\`html
20800
+ ${block.body}
20801
+ \`\`\``;
20802
+ if (block.type === "diagram") return `\`\`\`mermaid
20803
+ ${block.source}
20804
+ \`\`\``;
20805
+ const input = block.input;
20806
+ const options = input.kind === "single_choice" || input.kind === "multi_choice" ? `
20807
+ Options: ${input.options.map((o) => o.label).join(", ")}` : "";
20808
+ return `Q: ${block.prompt}${options}`;
20809
+ }).join("\n\n");
20810
+ }
20811
+ function buildCanvasModeGuidance() {
20812
+ return `Call \`whipped_show_canvas\` at most once per turn. Never call it twice in a row before the developer has replied \u2014 each call appends a new version to their canvas, so back-to-back calls show up as clutter, not a revision. If you want to reconsider before sending, do that thinking first and make one call with the version you're actually confident in.
20813
+
20814
+ Pick the right block type for what you're conveying: markdown for reasoning, steps, findings, and options; an \`html\` block whenever the developer wants to see UI, layout, or visual design \u2014 a dashboard, a page structure, a component arrangement. Don't default to describing a layout in prose when they asked to see it \u2014 build an actual mockup (divs, flexbox/grid, realistic spacing and colors) so they're looking at an approximation of the real thing, not reading about it. \`html\` blocks are injected into the page at runtime via \`dangerouslySetInnerHTML\` \u2014 they are NOT compiled by the app's build-time Tailwind setup, so Tailwind utility classes in that HTML (e.g. \`class="grid grid-cols-3 gap-4"\`) produce no CSS and render unstyled. That's a styling detail, not a reason to avoid html blocks \u2014 style mockups with inline \`style="..."\` attributes, or a \`<style>\` block scoped to unique ids/classes you define in that same block's body.
20815
+
20816
+ A question can be marked \`required\`, but that's a signal to you, not something the UI enforces \u2014 the developer can send feedback (or approve) without answering one, e.g. because they'd rather just leave a comment than pick from options that don't fit. The message you get back states every question explicitly, either with an answer or "(not answered)" \u2014 never silently omitted. If a required question comes back "(not answered)" and it's still something you need to know, ask it again in your next canvas version rather than assuming it's resolved. And if a comment on a question block says the options don't fit (wrong choices, missing one they want, etc.), revise, add, or remove options in your next version accordingly instead of re-asking the same broken question verbatim.
20817
+
20818
+ When the developer approves a canvas, they'll be offered the option to save it to the project's reusable canvas library. If they do, you'll be asked to consolidate everything proposed across every version pushed in this session into ONE final, coherent canvas and save it via the \`whipped_save_canvas\` tool. Beyond that one prompted moment, also call \`whipped_save_canvas\` proactively whenever you finish a meaningful chunk of work \u2014 describe what's done explicitly in the blocks (not just what's left), so that if this session's canvas is ever resumed later, its state accurately reflects progress. If this session already has a saved canvas (you resumed from one, or already saved once), calling the tool again updates that same canvas in place rather than creating a duplicate \u2014 you don't need to track which case applies, the tool handles it.`;
20819
+ }
20820
+
20821
+ // src/git/git-diff-utils.ts
20514
20822
  import { spawnSync as spawnSync5 } from "node:child_process";
20515
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
20516
- import { readdir, readFile as readFile2, stat, unlink } from "node:fs/promises";
20823
+ import { readFileSync as readFileSync6 } from "node:fs";
20517
20824
  import { join as join16 } from "node:path";
20825
+ function git4(args, cwd) {
20826
+ return spawnSync5("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).stdout?.trim() ?? "";
20827
+ }
20828
+ function readFileSafe(filePath) {
20829
+ try {
20830
+ return readFileSync6(filePath, "utf-8");
20831
+ } catch {
20832
+ return "";
20833
+ }
20834
+ }
20835
+ function getGitStat(worktreePath, baseRef) {
20836
+ const parts = [
20837
+ git4(["diff", "--stat", `${baseRef}...HEAD`], worktreePath),
20838
+ git4(["diff", "--stat", "--cached"], worktreePath),
20839
+ git4(["diff", "--stat"], worktreePath)
20840
+ ].filter(Boolean);
20841
+ const newUntracked = git4(["ls-files", "--others", "--exclude-standard"], worktreePath).split("\n").map((f2) => f2.trim()).filter(Boolean);
20842
+ if (newUntracked.length > 0) {
20843
+ parts.push(`New files:
20844
+ ${newUntracked.map((f2) => ` ${f2}`).join("\n")}`);
20845
+ }
20846
+ return parts.join("\n") || "(no changes detected \u2014 agent may not have committed yet)";
20847
+ }
20848
+ function getGitFullDiff(worktreePath, baseRef) {
20849
+ const sections = [];
20850
+ const diffParts = [
20851
+ git4(["diff", "-U15", `${baseRef}...HEAD`], worktreePath),
20852
+ git4(["diff", "-U15", "--cached"], worktreePath),
20853
+ git4(["diff", "-U15"], worktreePath)
20854
+ ].filter(Boolean);
20855
+ if (diffParts.length > 0) {
20856
+ sections.push(`\`\`\`diff
20857
+ ${diffParts.join("\n")}
20858
+ \`\`\``);
20859
+ }
20860
+ const newUntracked = git4(["ls-files", "--others", "--exclude-standard"], worktreePath).split("\n").map((f2) => f2.trim()).filter(Boolean);
20861
+ if (newUntracked.length > 0) {
20862
+ const newFileContents = [];
20863
+ for (const file of newUntracked) {
20864
+ const content = readFileSafe(join16(worktreePath, file));
20865
+ const ext = file.split(".").pop() ?? "";
20866
+ newFileContents.push(content ? `### ${file}
20867
+ \`\`\`${ext}
20868
+ ${content}
20869
+ \`\`\`` : `### ${file} (unreadable)`);
20870
+ }
20871
+ sections.push(`New files (full content):
20872
+
20873
+ ${newFileContents.join("\n\n")}`);
20874
+ }
20875
+ return sections.join("\n\n");
20876
+ }
20877
+ function getGitHeadSha(worktreePath) {
20878
+ return git4(["rev-parse", "HEAD"], worktreePath);
20879
+ }
20880
+ var DIFF_MAX_BUFFER = 4 * 1024 * 1024;
20881
+ function buildWorktreeDiff(worktreePath, baseRef) {
20882
+ const mergeBaseResult = spawnSync5("git", ["merge-base", baseRef, "HEAD"], {
20883
+ cwd: worktreePath,
20884
+ encoding: "utf-8"
20885
+ });
20886
+ if (mergeBaseResult.status !== 0) {
20887
+ return { diff: null, error: mergeBaseResult.stderr?.trim() || "Failed to resolve merge base" };
20888
+ }
20889
+ const diffResult = spawnSync5("git", ["diff", mergeBaseResult.stdout.trim(), "--no-color", "-U3"], {
20890
+ cwd: worktreePath,
20891
+ encoding: "utf-8",
20892
+ maxBuffer: DIFF_MAX_BUFFER
20893
+ });
20894
+ if (diffResult.status !== 0 && diffResult.stderr) {
20895
+ return { diff: null, error: diffResult.stderr.trim() };
20896
+ }
20897
+ const untrackedResult = spawnSync5("git", ["ls-files", "--others", "--exclude-standard"], {
20898
+ cwd: worktreePath,
20899
+ encoding: "utf-8"
20900
+ });
20901
+ const untrackedDiffs = (untrackedResult.stdout ?? "").split("\n").map((f2) => f2.trim()).filter(Boolean).map((file) => {
20902
+ const header = `diff --git a/${file} b/${file}
20903
+ new file mode 100644
20904
+ --- /dev/null
20905
+ +++ b/${file}`;
20906
+ const content = readFileSafe(join16(worktreePath, file));
20907
+ if (!content) return header;
20908
+ const lines = content.split("\n");
20909
+ const addedLines = lines.map((l) => `+${l}`).join("\n");
20910
+ return `${header}
20911
+ @@ -0,0 +1,${lines.length} @@
20912
+ ${addedLines}`;
20913
+ });
20914
+ const diff = [diffResult.stdout, ...untrackedDiffs].filter((s2) => s2?.trim()).join("\n");
20915
+ const behindResult = spawnSync5("git", ["rev-list", "--count", `HEAD..${baseRef}`], {
20916
+ cwd: worktreePath,
20917
+ encoding: "utf-8"
20918
+ });
20919
+ const baseBehindCount = parseInt(behindResult.stdout?.trim() ?? "0", 10) || 0;
20920
+ return { diff, error: null, baseBehindCount };
20921
+ }
20922
+ var INLINE_DIFF_LIMIT = 8e3;
20923
+ function formatDiffBlock(fullDiff, baseRef, header = "Git diff") {
20924
+ if (fullDiff.length <= INLINE_DIFF_LIMIT) return `${header}:
20925
+ ${fullDiff}`;
20926
+ return `Large changeset (${fullDiff.length.toLocaleString()} chars). Use \`git diff ${baseRef}...HEAD\` and read individual files to explore.`;
20927
+ }
20928
+
20929
+ // src/daemon/review-pipeline.ts
20930
+ import { existsSync as existsSync9 } from "node:fs";
20931
+ import { readdir, readFile as readFile2, stat, unlink } from "node:fs/promises";
20932
+ import { join as join17 } from "node:path";
20518
20933
  init_runtime_config();
20519
20934
  init_api_contract();
20520
20935
  init_logger();
@@ -20799,7 +21214,7 @@ function getSlotTriggerWord(type) {
20799
21214
  }
20800
21215
  var SCREENSHOT_EXTENSIONS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "webp"]);
20801
21216
  async function attachBrowserArtifacts(workspaceId, card, result, since) {
20802
- const dir = join16(ATTACHMENTS_DIR, card.id);
21217
+ const dir = join17(ATTACHMENTS_DIR, card.id);
20803
21218
  let entries;
20804
21219
  try {
20805
21220
  entries = await readdir(dir);
@@ -20812,7 +21227,7 @@ async function attachBrowserArtifacts(workspaceId, card, result, since) {
20812
21227
  for (const name of entries) {
20813
21228
  const ext = name.split(".").pop()?.toLowerCase() ?? "";
20814
21229
  if (!SCREENSHOT_EXTENSIONS.has(ext)) continue;
20815
- const filePath = join16(dir, name);
21230
+ const filePath = join17(dir, name);
20816
21231
  try {
20817
21232
  const info2 = await stat(filePath);
20818
21233
  if (!info2.isFile() || info2.mtimeMs < since) continue;
@@ -21077,80 +21492,25 @@ function runAgentOnce(agentId, prompt, cwd, workspaceId, streamId, stateHub, reg
21077
21492
  unregisterProcess = registerLiveProcess(streamId, proc);
21078
21493
  });
21079
21494
  }
21080
- function git4(args, cwd) {
21081
- return spawnSync5("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).stdout?.trim() ?? "";
21082
- }
21083
- function readFileSafe(filePath) {
21084
- try {
21085
- return readFileSync6(filePath, "utf-8");
21086
- } catch {
21087
- return "";
21088
- }
21495
+ function attachmentLines(attachments) {
21496
+ return attachments.map((a, i) => `- [Attachment #${i + 1}] ${a.name}: ${a.path}`).join("\n");
21089
21497
  }
21090
- function getGitStat(worktreePath, baseRef) {
21498
+ function formatComment(c, opts) {
21499
+ const typeLabel = c.type === "human" ? "Human Feedback" : c.type === "visual-comment" ? "Visual Feedback" : c.type.replace(/[-_]/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
21500
+ const actorId = c.actor.id;
21501
+ const statusLabel = c.status?.toUpperCase() ?? "";
21502
+ const hasMustFix = c.issues?.some((i) => i.severity === "blocking" || i.severity === "warning") ?? false;
21503
+ const failedRound = c.status === "fail" || hasMustFix;
21504
+ const mustFix = failedRound && !opts.stripMustFix ? " \u26A0 MUST FIX BEFORE PROCEEDING" : "";
21091
21505
  const parts = [
21092
- git4(["diff", "--stat", `${baseRef}...HEAD`], worktreePath),
21093
- git4(["diff", "--stat", "--cached"], worktreePath),
21094
- git4(["diff", "--stat"], worktreePath)
21095
- ].filter(Boolean);
21096
- const newUntracked = git4(["ls-files", "--others", "--exclude-standard"], worktreePath).split("\n").map((f2) => f2.trim()).filter(Boolean);
21097
- if (newUntracked.length > 0) {
21098
- parts.push(`New files:
21099
- ${newUntracked.map((f2) => ` ${f2}`).join("\n")}`);
21100
- }
21101
- return parts.join("\n") || "(no changes detected \u2014 agent may not have committed yet)";
21102
- }
21103
- function getGitFullDiff(worktreePath, baseRef) {
21104
- const sections = [];
21105
- const diffParts = [
21106
- git4(["diff", "-U15", `${baseRef}...HEAD`], worktreePath),
21107
- git4(["diff", "-U15", "--cached"], worktreePath),
21108
- git4(["diff", "-U15"], worktreePath)
21109
- ].filter(Boolean);
21110
- if (diffParts.length > 0) {
21111
- sections.push(`\`\`\`diff
21112
- ${diffParts.join("\n")}
21113
- \`\`\``);
21114
- }
21115
- const newUntracked = git4(["ls-files", "--others", "--exclude-standard"], worktreePath).split("\n").map((f2) => f2.trim()).filter(Boolean);
21116
- if (newUntracked.length > 0) {
21117
- const newFileContents = [];
21118
- for (const file of newUntracked) {
21119
- const content = readFileSafe(join16(worktreePath, file));
21120
- const ext = file.split(".").pop() ?? "";
21121
- newFileContents.push(content ? `### ${file}
21122
- \`\`\`${ext}
21123
- ${content}
21124
- \`\`\`` : `### ${file} (unreadable)`);
21125
- }
21126
- sections.push(`New files (full content):
21127
-
21128
- ${newFileContents.join("\n\n")}`);
21129
- }
21130
- return sections.join("\n\n");
21131
- }
21132
- function getGitHeadSha(worktreePath) {
21133
- return git4(["rev-parse", "HEAD"], worktreePath);
21134
- }
21135
- function attachmentLines(attachments) {
21136
- return attachments.map((a, i) => `- [Attachment #${i + 1}] ${a.name}: ${a.path}`).join("\n");
21137
- }
21138
- function formatComment(c, opts) {
21139
- const typeLabel = c.type === "human" ? "Human Feedback" : c.type === "visual-comment" ? "Visual Feedback" : c.type.replace(/[-_]/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
21140
- const actorId = c.actor.id;
21141
- const statusLabel = c.status?.toUpperCase() ?? "";
21142
- const hasMustFix = c.issues?.some((i) => i.severity === "blocking" || i.severity === "warning") ?? false;
21143
- const failedRound = c.status === "fail" || hasMustFix;
21144
- const mustFix = failedRound && !opts.stripMustFix ? " \u26A0 MUST FIX BEFORE PROCEEDING" : "";
21145
- const parts = [
21146
- `${opts.headingLevel} ${typeLabel} \xB7 ${actorId}${statusLabel ? ` \xB7 ${statusLabel}` : ""}${mustFix}`
21147
- ];
21148
- if (c.summary) parts.push(c.summary);
21149
- if (c.issues?.length) {
21150
- for (const issue of c.issues) {
21151
- const loc = issue.file ? `${issue.file}${issue.line != null ? `:${issue.line}` : ""}` : "";
21152
- parts.push(`- [${issue.severity}]${loc ? ` ${loc}` : ""} \u2014 ${issue.message}`);
21153
- }
21506
+ `${opts.headingLevel} ${typeLabel} \xB7 ${actorId}${statusLabel ? ` \xB7 ${statusLabel}` : ""}${mustFix}`
21507
+ ];
21508
+ if (c.summary) parts.push(c.summary);
21509
+ if (c.issues?.length) {
21510
+ for (const issue of c.issues) {
21511
+ const loc = issue.file ? `${issue.file}${issue.line != null ? `:${issue.line}` : ""}` : "";
21512
+ parts.push(`- [${issue.severity}]${loc ? ` ${loc}` : ""} \u2014 ${issue.message}`);
21513
+ }
21154
21514
  }
21155
21515
  if (c.attachments?.length) {
21156
21516
  parts.push(`Attached files (use Read tool to view):
@@ -21257,12 +21617,6 @@ ${sections.join("\n\n---\n\n")}`,
21257
21617
  files: []
21258
21618
  };
21259
21619
  }
21260
- var INLINE_DIFF_LIMIT = 8e3;
21261
- function formatDiffBlock(fullDiff, baseRef, header = "Git diff") {
21262
- if (fullDiff.length <= INLINE_DIFF_LIMIT) return `${header}:
21263
- ${fullDiff}`;
21264
- return `Large changeset (${fullDiff.length.toLocaleString()} chars). Use \`git diff ${baseRef}...HEAD\` and read individual files to explore.`;
21265
- }
21266
21620
  var FOLLOWUP_REVIEW_FOCUS = `**This is a follow-up review.** An earlier version of this work already passed review in a prior round \u2014 only the \`## New Feedback\` / \`## Current Iteration\` items triggered this run. Re-review ONLY: (1) whether those items were addressed in the changes below, and (2) whether the new changes introduced a regression or broke a caller (grep callers of anything touched to confirm). Do NOT re-litigate previously-approved code or raise issues unrelated to this round's feedback.`;
21267
21621
  function renderReviewDiff(stat3, fullDiff, baseRef, scope) {
21268
21622
  if (!scope.useIncremental) {
@@ -21764,6 +22118,64 @@ function tryParseAgentJson(output) {
21764
22118
  }
21765
22119
  }
21766
22120
 
22121
+ // src/daemon/companion-agent.ts
22122
+ function buildCompanionAgentSystemPrompt(workspaceId, repoPath, worktreePath, baseRef, secrets, systemPrompt, gitInstructions, seedPrompt, resumedCanvas) {
22123
+ const effectiveGitInstructions = gitInstructions?.trim() || DEFAULT_GIT_INSTRUCTIONS;
22124
+ const fullDiff = getGitFullDiff(worktreePath, baseRef);
22125
+ const worktreeSection = fullDiff ? `## Current worktree state (vs ${baseRef})
22126
+ ${getGitStat(worktreePath, baseRef)}
22127
+
22128
+ ## Diff (vs ${baseRef})
22129
+ ${formatDiffBlock(fullDiff, baseRef, "Git diff")}` : `## Worktree state
22130
+
22131
+ The worktree is clean and branched from \`${baseRef}\` \u2014 there is no diff yet. Skip \`git diff\` and start working.`;
22132
+ const parts = [
22133
+ `You are the Companion agent for the project at \`${repoPath}\`.
22134
+
22135
+ You are a direct, chat-driven pairing session with a developer. Unlike the ticket pipeline's dev agent, you are not working through a queued task with an automated reviewer downstream \u2014 the developer is talking to you live and steering the work turn by turn. You have full write access to the code in your current working directory, which is an isolated git worktree branched from \`${baseRef}\`.
22136
+
22137
+ Work incrementally and check in with the developer as you go rather than disappearing to complete a large scope autonomously. When you commit, follow the project's git conventions (see "## Git conventions" below) \u2014 do not commit until the developer asks you to, unless they've told you to commit as you go.`,
22138
+ worktreeSection
22139
+ ];
22140
+ parts.push(`## Sharing a canvas with the developer
22141
+
22142
+ When the developer asks you to "plan" something, wants to see a report, findings, or a set of questions answered \u2014 or you want to lay out an approach before starting \u2014 use the \`whipped_show_canvas\` MCP tool. That's what "plan" (and requests like it) means in this session: push it to the canvas, don't just describe it in chat. Do NOT use any other built-in planning mode you might have; always push through this tool instead, even for what would normally trigger that. Push markdown, raw HTML, mermaid diagrams, and interactive questions \u2014 instead of writing a long response as a chat message \u2014 whenever you want structured feedback. The developer's answers, comments, and notes come back as a normal follow-up message in this conversation \u2014 there is no separate response channel, so treat it exactly like something they typed.
22143
+
22144
+ ${buildCanvasModeGuidance()}`);
22145
+ if (resumedCanvas) {
22146
+ parts.push(`## Resuming a saved canvas
22147
+
22148
+ This session was started from a previously saved canvas titled "${resumedCanvas.title}". Its content is shown in full below \u2014 the developer can already see this in their canvas as version 1, but you cannot read it back, so this is the only place you'll see it. Treat it as the current state of the work: continue from here rather than re-planning from scratch, and call \`whipped_save_canvas\` again as you make further progress so the saved canvas stays in sync with what's actually done.
22149
+
22150
+ ${serializeCanvasBlocksForPrompt(resumedCanvas.blocks)}`);
22151
+ }
22152
+ if (seedPrompt?.trim()) parts.push(`## Project-specific instructions
22153
+
22154
+ ${seedPrompt.trim()}`);
22155
+ parts.push(`## Memory
22156
+
22157
+ This project has its own persistent memory. The \`whipped_save_memory\` / \`whipped_update_memory\` MCP tools ARE this project's memory \u2014 do NOT use your own notes, scratch files, CLAUDE.md, or any other memory system for durable facts.
22158
+
22159
+ When you are asked to "remember", "save to memory", "note for next time" \u2014 or you hit a cross-cutting convention, an architecture decision, a non-obvious repo-wide gotcha, or a correction the developer made \u2014 record it in memory. Do NOT record what is already in the code or schema (endpoint request/response shapes, query params, column lists, field names, colour classes, per-page layout): if your note would cite the file where the truth lives, the file is the memory \u2014 skip it. Keep each entry to one focused fact in 1-3 sentences.
22160
+
22161
+ Before recording, check the memory list injected above (each entry shows its \`[id]\`) and \`whipped_search_memory\`. If what you're recording **contradicts, reverses, supersedes, corrects, or is a near-duplicate of** an existing memory, call \`whipped_update_memory\` with that memory's id and overwrite it \u2014 do NOT create a second, conflicting entry.
22162
+
22163
+ Scope a memory \`project\` for facts specific to this repo, or \`global\` for things that apply across all the user's projects (style/preferences).`);
22164
+ const secretsSection = buildSecretsSection(secrets);
22165
+ if (secretsSection) parts.push(secretsSection);
22166
+ if (systemPrompt?.trim()) parts.push(`## Project context
22167
+
22168
+ ${systemPrompt.trim()}`);
22169
+ parts.push(`## Git conventions
22170
+
22171
+ ${effectiveGitInstructions}`);
22172
+ const memContext = buildMemoryContext(workspaceId);
22173
+ const text = parts.join("\n\n");
22174
+ return memContext ? `${memContext}
22175
+
22176
+ ${text}` : text;
22177
+ }
22178
+
21767
22179
  // src/daemon/scheduler.ts
21768
22180
  var FAST_EXIT_THRESHOLD_MS = 8e3;
21769
22181
  var MAX_RECENT_BUFFERS = 100;
@@ -21774,6 +22186,7 @@ var TaskScheduler = class {
21774
22186
  options;
21775
22187
  running = /* @__PURE__ */ new Map();
21776
22188
  assistantSessions = /* @__PURE__ */ new Map();
22189
+ companionSessions = /* @__PURE__ */ new Map();
21777
22190
  // Keep the last output buffer around after a task exits so the terminal
21778
22191
  // can still restore when the user opens it for a completed/awaiting-review task.
21779
22192
  recentBuffers = /* @__PURE__ */ new Map();
@@ -21829,7 +22242,7 @@ var TaskScheduler = class {
21829
22242
  get assistantAgentTaskId() {
21830
22243
  return `${ASSISTANT_AGENT_PREFIX}${this.options.workspaceId}`;
21831
22244
  }
21832
- async startAssistantAgent() {
22245
+ async startAssistantAgent(override, savedCanvasId) {
21833
22246
  const { workspaceId, repoPath, serverUrl, stateHub, defaultAgent } = this.options;
21834
22247
  const taskId = this.assistantAgentTaskId;
21835
22248
  const existing = this.assistantSessions.get(taskId);
@@ -21841,11 +22254,18 @@ var TaskScheduler = class {
21841
22254
  stateHub.clearTerminalBuffer(workspaceId, taskId);
21842
22255
  const prompt = "";
21843
22256
  const projectConfig = await loadProjectConfig(workspaceId);
21844
- const assistantModel = projectConfig.assistantModel;
22257
+ const assistantModel = override ?? projectConfig.assistantModel;
21845
22258
  const agentId = assistantModel?.agentId ?? defaultAgent;
21846
22259
  const secrets = projectConfig.secrets ?? [];
21847
22260
  const secretsEnv = buildSecretsEnv(secrets);
21848
- const assistantSystemPrompt = buildAssistantAgentSystemPrompt(repoPath, secrets, projectConfig.systemPrompt);
22261
+ const savedCanvas = savedCanvasId ? getCompanionSavedCanvas(savedCanvasId) : null;
22262
+ if (savedCanvas) createCompanionCanvas(taskId, workspaceId, savedCanvas.blocks);
22263
+ const assistantSystemPrompt = buildAssistantAgentSystemPrompt(
22264
+ repoPath,
22265
+ secrets,
22266
+ projectConfig.systemPrompt,
22267
+ savedCanvas ? { title: savedCanvas.title, blocks: savedCanvas.blocks } : void 0
22268
+ );
21849
22269
  const memContext = buildMemoryContext(workspaceId);
21850
22270
  const appendSystemPrompt = memContext ? `${memContext}
21851
22271
 
@@ -21896,6 +22316,9 @@ ${assistantSystemPrompt}` : assistantSystemPrompt;
21896
22316
  ...agentId === "cursor" ? { [CURSOR_CONFIG_DIR_ENV]: getCursorConfigDir(taskId) } : {}
21897
22317
  },
21898
22318
  mcpConfigPath: agentId === "claude" ? CLAUDE_ASSISTANT_MCP_CONFIG_PATH : void 0,
22319
+ // Denies Claude's own native plan-mode tools — see writeClaudeCompanionSettings
22320
+ // — so "plan" always means whipped_show_canvas, same as the companion agent.
22321
+ hookSettingsPath: agentId === "claude" ? CLAUDE_COMPANION_SETTINGS_PATH : void 0,
21899
22322
  mcpServer: agentId === "codex" ? buildWhippedMcpServerSpec(
21900
22323
  getMcpServerPath(),
21901
22324
  serverUrl,
@@ -21932,6 +22355,234 @@ ${assistantSystemPrompt}` : assistantSystemPrompt;
21932
22355
  isAssistantAgentRunning() {
21933
22356
  return this.assistantSessions.has(this.assistantAgentTaskId);
21934
22357
  }
22358
+ // Creates the worktree (or resolves the main-repo checkout) synchronously and
22359
+ // returns quickly — install + agent spawn happen in the background via
22360
+ // launchCompanionAgent so callers (the create-session API request) aren't
22361
+ // blocked on a potentially long install command.
22362
+ async startCompanionAgent(session) {
22363
+ const { workspaceId, repoPath, stateHub } = this.options;
22364
+ const taskId = session.id;
22365
+ const existing = this.companionSessions.get(taskId);
22366
+ if (existing) existing.process.kill();
22367
+ this.recentBuffers.delete(taskId);
22368
+ stateHub.clearTerminalBuffer(workspaceId, taskId);
22369
+ if (session.useWorktree) {
22370
+ const worktree = createWorktree(taskId, repoPath, session.baseRef, session.branchName ?? void 0);
22371
+ setCompanionSessionWorktreePath(taskId, worktree.path);
22372
+ setCompanionSessionStatus(taskId, worktree.isNew ? "installing" : "running");
22373
+ void this.launchCompanionAgent(session, worktree.path, worktree.isNew);
22374
+ } else {
22375
+ setCompanionSessionWorktreePath(taskId, repoPath);
22376
+ setCompanionSessionStatus(taskId, "running");
22377
+ void this.launchCompanionAgent(session, repoPath, false);
22378
+ }
22379
+ }
22380
+ async launchCompanionAgent(session, cwd, isNewWorktree) {
22381
+ const { workspaceId, repoPath, serverUrl, stateHub } = this.options;
22382
+ const taskId = session.id;
22383
+ const agentId = session.agentId;
22384
+ const projectConfig = await loadProjectConfig(workspaceId);
22385
+ const secrets = projectConfig.secrets ?? [];
22386
+ const secretsEnv = buildSecretsEnv(secrets);
22387
+ if (isNewWorktree && projectConfig.worktreeSetup) {
22388
+ await this.runCompanionInstall(taskId, workspaceId, repoPath, cwd, projectConfig.worktreeSetup);
22389
+ if (this.isShuttingDown) return;
22390
+ if (this.manuallyStoppedInstalls.delete(taskId)) {
22391
+ await removeWorktreeAsync(taskId, repoPath, session.branchName ?? void 0);
22392
+ setCompanionSessionWorktreePath(taskId, null);
22393
+ setCompanionSessionStatus(taskId, "stopped");
22394
+ return;
22395
+ }
22396
+ }
22397
+ setCompanionSessionStatus(taskId, "running");
22398
+ const resumedCanvas = session.savedCanvasId ? getCompanionSavedCanvas(session.savedCanvasId) : null;
22399
+ const appendSystemPrompt = buildCompanionAgentSystemPrompt(
22400
+ workspaceId,
22401
+ repoPath,
22402
+ cwd,
22403
+ session.baseRef,
22404
+ secrets,
22405
+ projectConfig.systemPrompt,
22406
+ projectConfig.gitInstructions,
22407
+ session.seedPrompt,
22408
+ resumedCanvas ? { title: resumedCanvas.title, blocks: resumedCanvas.blocks } : void 0
22409
+ );
22410
+ const mcpConfigPath = !isPluginConfigAgent(agentId) && agentId !== "cursor" ? getMcpConfigPath(taskId) : void 0;
22411
+ if (agentId === "claude") {
22412
+ await writeClaudeMcpConfig(
22413
+ getMcpServerPath(),
22414
+ serverUrl,
22415
+ workspaceId,
22416
+ agentId,
22417
+ mcpConfigPath,
22418
+ void 0,
22419
+ buildMcpRoleArgs("companion", void 0, taskId)
22420
+ ).catch((err) => {
22421
+ logger.warn({ err }, "[scheduler] Failed to write companion agent MCP config");
22422
+ });
22423
+ } else if (isPluginConfigAgent(agentId)) {
22424
+ const mcpSpec = buildWhippedMcpServerSpec(
22425
+ getMcpServerPath(),
22426
+ serverUrl,
22427
+ workspaceId,
22428
+ agentId,
22429
+ buildMcpRoleArgs("companion", void 0, taskId)
22430
+ );
22431
+ await writePluginAgentFiles(agentId, taskId, getServerPort(serverUrl), mcpSpec, { appendSystemPrompt }).catch(
22432
+ (err) => {
22433
+ logger.warn({ err }, `[scheduler] Failed to write ${agentId} companion agent files`);
22434
+ }
22435
+ );
22436
+ } else if (agentId === "cursor") {
22437
+ const mcpSpec = buildWhippedMcpServerSpec(
22438
+ getMcpServerPath(),
22439
+ serverUrl,
22440
+ workspaceId,
22441
+ agentId,
22442
+ buildMcpRoleArgs("companion", void 0, taskId)
22443
+ );
22444
+ await writeCursorConfigFiles(taskId, getServerPort(serverUrl), mcpSpec).catch((err) => {
22445
+ logger.warn({ err }, "[scheduler] Failed to write cursor companion agent config");
22446
+ });
22447
+ }
22448
+ let resolveExit;
22449
+ const exitPromise = new Promise((resolve5) => {
22450
+ resolveExit = resolve5;
22451
+ });
22452
+ const companionTask = {
22453
+ taskId,
22454
+ streamId: taskId,
22455
+ // companion session uses taskId as its stream (single persistent session)
22456
+ agentId,
22457
+ exitPromise,
22458
+ process: spawnAgent({
22459
+ agentId,
22460
+ prompt: "",
22461
+ cwd,
22462
+ env: {
22463
+ ...secretsEnv,
22464
+ ...buildTaskHookEnv(taskId, workspaceId),
22465
+ WHIPPED_SLOT: "companion",
22466
+ ...pluginAgentConfigDirEnv(agentId, taskId),
22467
+ ...agentId === "cursor" ? { [CURSOR_CONFIG_DIR_ENV]: getCursorConfigDir(taskId) } : {}
22468
+ },
22469
+ mcpConfigPath: agentId === "claude" ? mcpConfigPath : void 0,
22470
+ // Denies Claude's own native plan-mode tools for this session — see
22471
+ // writeClaudeCompanionSettings — so "plan" always means whipped_show_canvas.
22472
+ hookSettingsPath: agentId === "claude" ? CLAUDE_COMPANION_SETTINGS_PATH : void 0,
22473
+ mcpServer: agentId === "codex" ? buildWhippedMcpServerSpec(
22474
+ getMcpServerPath(),
22475
+ serverUrl,
22476
+ workspaceId,
22477
+ agentId,
22478
+ buildMcpRoleArgs("companion", void 0, taskId)
22479
+ ) : void 0,
22480
+ model: session.model ?? null,
22481
+ effort: session.effort ?? null,
22482
+ appendSystemPrompt: isPluginConfigAgent(agentId) ? void 0 : appendSystemPrompt,
22483
+ onOutput: (data) => {
22484
+ companionTask.outputBuffer += data;
22485
+ stateHub.broadcastTerminalOutput(workspaceId, taskId, data);
22486
+ },
22487
+ onExit: () => {
22488
+ this.setRecentBuffer(taskId, companionTask.outputBuffer);
22489
+ this.companionSessions.delete(taskId);
22490
+ setCompanionSessionStatus(taskId, "stopped");
22491
+ resolveExit();
22492
+ }
22493
+ }),
22494
+ startedAt: Date.now(),
22495
+ outputBuffer: ""
22496
+ };
22497
+ this.companionSessions.set(taskId, companionTask);
22498
+ }
22499
+ // Copies/links worktreeSetup's configured files then runs its install command,
22500
+ // mirroring the card dev-agent's worktree setup step (launchDevAgent above) but
22501
+ // streamed straight into the companion session's own terminal (taskId) instead
22502
+ // of a separate install stream row, since companion sessions have no such table.
22503
+ async runCompanionInstall(taskId, workspaceId, repoPath, worktreePath, worktreeSetup) {
22504
+ const { stateHub } = this.options;
22505
+ const { filesToCopy, installCommand } = worktreeSetup;
22506
+ for (const entry of filesToCopy) {
22507
+ const src = join18(repoPath, entry.path);
22508
+ if (!existsSync10(src)) continue;
22509
+ const dst = join18(worktreePath, entry.path);
22510
+ await mkdir3(dirname6(dst), { recursive: true });
22511
+ try {
22512
+ if (entry.symlink) {
22513
+ await shareIntoWorktree(src, dst);
22514
+ } else {
22515
+ await cp(src, dst, { recursive: true, dereference: true });
22516
+ }
22517
+ } catch (err) {
22518
+ logger.warn(
22519
+ { err },
22520
+ `[scheduler] companion worktree setup: failed to ${entry.symlink ? "link" : "copy"} ${entry.path}`
22521
+ );
22522
+ }
22523
+ }
22524
+ const installCmd = installCommand.trim();
22525
+ if (!installCmd) return;
22526
+ let buffer = "";
22527
+ const emit = (data) => {
22528
+ buffer += data;
22529
+ this.recentBuffers.set(taskId, buffer);
22530
+ stateHub.broadcastTerminalOutput(workspaceId, taskId, data);
22531
+ };
22532
+ emit(`\x1B[1;36m$ ${installCmd}\x1B[0m\r
22533
+ `);
22534
+ const exitCode = await new Promise((resolveExit) => {
22535
+ const [shell, shellArgs] = getShellInvocation(installCmd);
22536
+ const proc = nodePty2.spawn(shell, shellArgs, {
22537
+ name: "xterm-256color",
22538
+ cols: 220,
22539
+ rows: 50,
22540
+ cwd: worktreePath,
22541
+ env: { ...process.env, REPO_PATH: repoPath, TERM: "xterm-256color" }
22542
+ });
22543
+ this.runningInstalls.set(taskId, proc);
22544
+ proc.onData(emit);
22545
+ proc.onExit(({ exitCode: exitCode2 }) => resolveExit(exitCode2 ?? 0));
22546
+ });
22547
+ this.runningInstalls.delete(taskId);
22548
+ if (this.isShuttingDown) return;
22549
+ if (this.manuallyStoppedInstalls.has(taskId)) {
22550
+ emit("\r\n\x1B[1;33mInstall stopped\x1B[0m\r\n");
22551
+ return;
22552
+ }
22553
+ if (exitCode !== 0) {
22554
+ logger.error(`[scheduler] Install command failed (code ${exitCode}) for companion session ${taskId}`);
22555
+ emit(`\r
22556
+ \x1B[1;31mInstall command failed (code ${exitCode}) \u2014 proceeding anyway\x1B[0m\r
22557
+ `);
22558
+ } else {
22559
+ emit("\r\n\x1B[1;32mInstall complete\x1B[0m\r\n");
22560
+ }
22561
+ }
22562
+ // Kills the session's process (or its still-running install command) and waits
22563
+ // for it to actually exit (bounded by a timeout) before returning — callers
22564
+ // that are about to delete or merge the worktree on disk must await this
22565
+ // first, since attemptMerge/removeWorktreeAsync can otherwise race a still-live
22566
+ // process whose cwd is inside that worktree.
22567
+ async stopCompanionAgent(sessionId) {
22568
+ const installProc = this.runningInstalls.get(sessionId);
22569
+ if (installProc) {
22570
+ this.manuallyStoppedInstalls.add(sessionId);
22571
+ this.runningInstalls.delete(sessionId);
22572
+ killInstallProcess(installProc);
22573
+ }
22574
+ const session = this.companionSessions.get(sessionId);
22575
+ if (session) {
22576
+ const exitPromise = session.exitPromise ?? Promise.resolve();
22577
+ session.process.kill();
22578
+ await Promise.race([exitPromise, new Promise((resolve5) => setTimeout(resolve5, 5e3))]);
22579
+ }
22580
+ this.companionSessions.delete(sessionId);
22581
+ setCompanionSessionStatus(sessionId, "stopped");
22582
+ }
22583
+ isCompanionAgentRunning(sessionId) {
22584
+ return this.companionSessions.has(sessionId);
22585
+ }
21935
22586
  get activeCount() {
21936
22587
  return this.running.size;
21937
22588
  }
@@ -22120,9 +22771,9 @@ ${assistantSystemPrompt}` : assistantSystemPrompt;
22120
22771
  const copied = [];
22121
22772
  const linked = [];
22122
22773
  for (const entry of filesToCopy) {
22123
- const src = join17(repoPath, entry.path);
22774
+ const src = join18(repoPath, entry.path);
22124
22775
  if (!existsSync10(src)) continue;
22125
- const dst = join17(worktree.path, entry.path);
22776
+ const dst = join18(worktree.path, entry.path);
22126
22777
  await mkdir3(dirname6(dst), { recursive: true });
22127
22778
  try {
22128
22779
  if (entry.symlink) {
@@ -22542,6 +23193,8 @@ ${devSystemPromptResult.text}`;
22542
23193
  }
22543
23194
  const assistantSession = this.assistantSessions.get(streamId);
22544
23195
  if (assistantSession) return assistantSession.outputBuffer;
23196
+ const companionSession = this.companionSessions.get(streamId);
23197
+ if (companionSession) return companionSession.outputBuffer;
22545
23198
  return this.recentBuffers.get(streamId) ?? null;
22546
23199
  }
22547
23200
  resizeTerminal(streamId, cols, rows) {
@@ -22558,7 +23211,7 @@ ${devSystemPromptResult.text}`;
22558
23211
  for (const task of this.running.values()) {
22559
23212
  if (task.streamId === streamId) return task.process;
22560
23213
  }
22561
- return this.assistantSessions.get(streamId)?.process ?? this.liveProcesses.get(streamId);
23214
+ return this.assistantSessions.get(streamId)?.process ?? this.companionSessions.get(streamId)?.process ?? this.liveProcesses.get(streamId);
22562
23215
  }
22563
23216
  async handleHookEvent(event, taskId) {
22564
23217
  const { workspaceId, stateHub } = this.options;
@@ -22744,6 +23397,10 @@ ${devSystemPromptResult.text}`;
22744
23397
  }
22745
23398
  this.runningInstalls.clear();
22746
23399
  this.stopAssistantAgent();
23400
+ for (const [, task] of this.companionSessions) {
23401
+ task.process.kill();
23402
+ }
23403
+ this.companionSessions.clear();
22747
23404
  }
22748
23405
  // Call before stopAll() during graceful shutdown so onExit handlers bail out
22749
23406
  // and do not overwrite the failed/todo state written by cleanupStaleTasks().
@@ -22789,8 +23446,15 @@ function getMcpServerPath() {
22789
23446
  args: [resolve2(thisDir, "mcp-server.js")]
22790
23447
  };
22791
23448
  }
22792
- function buildAssistantAgentSystemPrompt(repoPath, secrets = [], systemPrompt) {
23449
+ function buildAssistantAgentSystemPrompt(repoPath, secrets = [], systemPrompt, resumedCanvas) {
22793
23450
  const secretsSection = buildSecretsSection(secrets);
23451
+ const resumedCanvasSection = resumedCanvas ? `
23452
+
23453
+ # Resuming a saved canvas
23454
+
23455
+ This conversation was started from a previously saved canvas titled "${resumedCanvas.title}". Its content is shown in full below \u2014 the developer can already see this in their canvas as version 1, but you cannot read it back, so this is the only place you'll see it. Treat it as the current state of the discussion: continue from here rather than re-planning from scratch, and call \`whipped_save_canvas\` again as things progress so the saved canvas stays in sync.
23456
+
23457
+ ${serializeCanvasBlocksForPrompt(resumedCanvas.blocks)}` : "";
22794
23458
  return `You are the Assistant for the project at \`${repoPath}\`.
22795
23459
 
22796
23460
  You are a conversational project assistant. You can discuss the project, help plan work, answer questions about the codebase, workflows, and board state, and help the developer decide what to build. You also have full control over the Kanban board and workflows via MCP tools.
@@ -22824,6 +23488,10 @@ You are a conversational project assistant. You can discuss the project, help pl
22824
23488
  - \`kanban_get_workflows\` \u2014 list all workflows (task and story/orch) with their agent slots, model tiers, tools, and prompts
22825
23489
  - \`kanban_upsert_workflow\` \u2014 create or fully replace a workflow (pass complete workflow object)
22826
23490
 
23491
+ ## Canvas
23492
+ - \`whipped_show_canvas\` \u2014 push a structured, interactive canvas (markdown, HTML mockups, mermaid diagrams, questions) \u2014 for a plan, a report, findings, or anything else worth showing rather than describing in chat
23493
+ - \`whipped_save_canvas\` \u2014 consolidate the conversation's canvas versions into one and save it to the reusable canvas library
23494
+
22827
23495
  ## Memory
22828
23496
  - \`whipped_search_memory\` \u2014 search durable project + global memory before re-discovering how something works
22829
23497
  - \`whipped_get_memory\` \u2014 fetch one memory's full content by id
@@ -22832,6 +23500,12 @@ You are a conversational project assistant. You can discuss the project, help pl
22832
23500
 
22833
23501
  The Memory section injected above this prompt lists existing memories with their \`[id]\`. When the developer asks you to "remember" something, or states a durable preference/decision, save it. Before saving, check the injected list and \`whipped_search_memory\`; if it contradicts or supersedes an existing entry, \`whipped_update_memory\` that id instead of creating a duplicate.
22834
23502
 
23503
+ # Sharing a canvas
23504
+
23505
+ When you want to lay out an approach, a UI mockup, or gather structured feedback before creating tickets \u2014 or the developer asks you to "plan" something, wants a report, or wants a set of questions answered \u2014 use the \`whipped_show_canvas\` MCP tool instead of writing a long response as a chat message. Do NOT use any other built-in planning mode you might have; always push it through this tool instead, even for what would normally trigger that. The developer's answers, comments, and notes come back as a normal follow-up message in this conversation \u2014 there is no separate response channel, so treat it exactly like something they typed.
23506
+
23507
+ ${buildCanvasModeGuidance()}${resumedCanvasSection}
23508
+
22835
23509
  # Card types
22836
23510
 
22837
23511
  **task** \u2014 a normal development ticket. Runs an optional plan slot, then dev, then any number of review slots (based on workflow).
@@ -25379,6 +26053,7 @@ var errorHandler2 = (err, c) => {
25379
26053
  };
25380
26054
 
25381
26055
  // src/api/routes/agent.ts
26056
+ init_api_contract();
25382
26057
  import { z as z3 } from "zod";
25383
26058
 
25384
26059
  // node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/utils/cookie.js
@@ -25676,8 +26351,8 @@ var zv = (target, schema) => zValidator(target, schema, (result) => {
25676
26351
  });
25677
26352
 
25678
26353
  // src/api/services/agent-service.ts
25679
- var startAgentSession = async (scheduler) => ({
25680
- taskId: await scheduler.startAssistantAgent()
26354
+ var startAgentSession = async (scheduler, override, savedCanvasId) => ({
26355
+ taskId: await scheduler.startAssistantAgent(override, savedCanvasId)
25681
26356
  });
25682
26357
  var stopAgentSession = async (scheduler) => {
25683
26358
  scheduler?.stopAssistantAgent();
@@ -25691,21 +26366,26 @@ var getAgentSessionStatus = async (scheduler) => {
25691
26366
  };
25692
26367
 
25693
26368
  // src/api/routes/agent.ts
26369
+ var startSessionBodySchema = z3.object({
26370
+ workspaceId: z3.string(),
26371
+ override: agentModelChoiceSchema.optional(),
26372
+ savedCanvasId: z3.string().optional()
26373
+ });
25694
26374
  var agentController = new Hono2().get("/session", zv("query", z3.object({ workspaceId: z3.string() })), async (c) => {
25695
26375
  const ctx = c.var.ctx;
25696
26376
  const { workspaceId } = c.req.valid("query");
25697
26377
  return c.json(await getAgentSessionStatus(ctx.getScheduler(workspaceId)));
25698
- }).post("/session", zv("json", z3.object({ workspaceId: z3.string() })), async (c) => {
26378
+ }).post("/session", zv("json", startSessionBodySchema), async (c) => {
25699
26379
  const ctx = c.var.ctx;
25700
- const { workspaceId } = c.req.valid("json");
26380
+ const { workspaceId, override, savedCanvasId } = c.req.valid("json");
25701
26381
  const scheduler = ctx.getScheduler(workspaceId);
25702
26382
  if (!scheduler) {
25703
26383
  await ctx.ensureWorkspace(workspaceId);
25704
26384
  const retried = ctx.getScheduler(workspaceId);
25705
26385
  if (!retried) throw NotFoundError("Workspace");
25706
- return c.json(await startAgentSession(retried));
26386
+ return c.json(await startAgentSession(retried, override, savedCanvasId));
25707
26387
  }
25708
- return c.json(await startAgentSession(scheduler));
26388
+ return c.json(await startAgentSession(scheduler, override, savedCanvasId));
25709
26389
  }).delete("/session", zv("query", z3.object({ workspaceId: z3.string() })), async (c) => {
25710
26390
  const ctx = c.var.ctx;
25711
26391
  const { workspaceId } = c.req.valid("query");
@@ -26251,57 +26931,11 @@ var getDiffService = async (workspaceId, cardId) => {
26251
26931
  const card = board.cards[cardId];
26252
26932
  if (!card) throw NotFoundError("Card");
26253
26933
  const worktreePath = getWorktreePath(resolveWorktreeOwnerId(cardId, board.cards));
26254
- const { existsSync: existsSync15 } = await import("node:fs");
26255
- if (!existsSync15(worktreePath)) {
26934
+ const { existsSync: existsSync16 } = await import("node:fs");
26935
+ if (!existsSync16(worktreePath)) {
26256
26936
  return { diff: null, error: "No worktree \u2014 agent has not started yet" };
26257
26937
  }
26258
- const committedResult = spawnSync6("git", ["diff", `${card.baseRef}...HEAD`, "--no-color", "-U3"], {
26259
- cwd: worktreePath,
26260
- encoding: "utf-8",
26261
- maxBuffer: 4 * 1024 * 1024
26262
- });
26263
- if (committedResult.status !== 0 && committedResult.stderr) {
26264
- return { diff: null, error: committedResult.stderr.trim() };
26265
- }
26266
- const stagedResult = spawnSync6("git", ["diff", "--cached", "--no-color", "-U3"], {
26267
- cwd: worktreePath,
26268
- encoding: "utf-8",
26269
- maxBuffer: 4 * 1024 * 1024
26270
- });
26271
- const unstagedResult = spawnSync6("git", ["diff", "--no-color", "-U3"], {
26272
- cwd: worktreePath,
26273
- encoding: "utf-8",
26274
- maxBuffer: 4 * 1024 * 1024
26275
- });
26276
- const untrackedResult = spawnSync6("git", ["ls-files", "--others", "--exclude-standard"], {
26277
- cwd: worktreePath,
26278
- encoding: "utf-8"
26279
- });
26280
- const untrackedFiles = (untrackedResult.stdout ?? "").split("\n").map((f2) => f2.trim()).filter(Boolean);
26281
- const { readFileSync: readFileSync9 } = await import("node:fs");
26282
- const untrackedDiffs = untrackedFiles.map((file) => {
26283
- try {
26284
- const content = readFileSync9(`${worktreePath}/${file}`, "utf-8");
26285
- const lines = content.split("\n");
26286
- const addedLines = lines.map((l, _i) => `+${l}`).join("\n");
26287
- const hunkHeader = `@@ -0,0 +1,${lines.length} @@`;
26288
- return `diff --git a/${file} b/${file}
26289
- new file mode 100644
26290
- --- /dev/null
26291
- +++ b/${file}
26292
- ${hunkHeader}
26293
- ${addedLines}`;
26294
- } catch {
26295
- return null;
26296
- }
26297
- }).filter((d) => d !== null);
26298
- const diff = [committedResult.stdout, stagedResult.stdout, unstagedResult.stdout, ...untrackedDiffs].filter((s2) => s2?.trim()).join("\n");
26299
- const behindResult = spawnSync6("git", ["rev-list", "--count", `HEAD..${card.baseRef}`], {
26300
- cwd: worktreePath,
26301
- encoding: "utf-8"
26302
- });
26303
- const baseBehindCount = parseInt(behindResult.stdout?.trim() ?? "0", 10) || 0;
26304
- return { diff, error: null, baseBehindCount };
26938
+ return buildWorktreeDiff(worktreePath, card.baseRef);
26305
26939
  };
26306
26940
  var getCommitsService = async (workspaceId, cardId) => {
26307
26941
  const workspaces = await listWorkspaces();
@@ -26311,8 +26945,8 @@ var getCommitsService = async (workspaceId, cardId) => {
26311
26945
  const card = board.cards[cardId];
26312
26946
  if (!card) throw NotFoundError("Card");
26313
26947
  const worktreePath = getWorktreePath(resolveWorktreeOwnerId(cardId, board.cards));
26314
- const { existsSync: existsSync15 } = await import("node:fs");
26315
- if (!existsSync15(worktreePath)) return { commits: [] };
26948
+ const { existsSync: existsSync16 } = await import("node:fs");
26949
+ if (!existsSync16(worktreePath)) return { commits: [] };
26316
26950
  const result = spawnSync6("git", ["log", "--pretty=format:%H%x00%h%x00%s%x00%an%x00%ai", `${card.baseRef}..HEAD`], {
26317
26951
  cwd: worktreePath,
26318
26952
  encoding: "utf-8"
@@ -26333,8 +26967,8 @@ var getDiffForCommitService = async (workspaceId, cardId, commitHash) => {
26333
26967
  if (!card) throw NotFoundError("Card");
26334
26968
  if (!/^[0-9a-f]{4,64}$/i.test(commitHash)) return { diff: null, error: "Invalid commit hash" };
26335
26969
  const worktreePath = getWorktreePath(resolveWorktreeOwnerId(cardId, board.cards));
26336
- const { existsSync: existsSync15 } = await import("node:fs");
26337
- if (!existsSync15(worktreePath)) return { diff: null, error: "No worktree" };
26970
+ const { existsSync: existsSync16 } = await import("node:fs");
26971
+ if (!existsSync16(worktreePath)) return { diff: null, error: "No worktree" };
26338
26972
  const result = spawnSync6("git", ["show", commitHash, "--format=", "--patch", "--no-color", "-U3"], {
26339
26973
  cwd: worktreePath,
26340
26974
  encoding: "utf-8",
@@ -26582,6 +27216,330 @@ var cardsController = new Hono2().post("/", zv("json", runtimeCardCreateRequestS
26582
27216
  }
26583
27217
  );
26584
27218
 
27219
+ // src/api/routes/companion-saved-canvases.ts
27220
+ import { z as z6 } from "zod";
27221
+
27222
+ // src/api/services/companion-saved-canvases-service.ts
27223
+ async function listCompanionSavedCanvasesEntry(workspaceId) {
27224
+ return { canvases: listCompanionSavedCanvases(workspaceId) };
27225
+ }
27226
+ async function deleteCompanionSavedCanvasEntry(id) {
27227
+ deleteCompanionSavedCanvas(id);
27228
+ }
27229
+ async function clearCompanionCanvasesEntry(sessionId) {
27230
+ deleteCompanionCanvasesForSession(sessionId);
27231
+ }
27232
+ async function saveCompanionCanvasEntry(sessionId, workspaceId, title, blocks) {
27233
+ const session = getCompanionSession(sessionId);
27234
+ if (session) {
27235
+ if (session.savedCanvasId) {
27236
+ const updated = updateCompanionSavedCanvas(session.savedCanvasId, { title, blocks });
27237
+ if (updated) return updated;
27238
+ }
27239
+ const created = createCompanionSavedCanvas(workspaceId, { title, blocks, sourceSessionId: sessionId });
27240
+ setCompanionSessionSavedCanvasId(sessionId, created.id);
27241
+ return created;
27242
+ }
27243
+ const existing = findCompanionSavedCanvasBySourceSession(sessionId);
27244
+ if (existing) {
27245
+ const updated = updateCompanionSavedCanvas(existing.id, { title, blocks });
27246
+ if (updated) return updated;
27247
+ }
27248
+ return createCompanionSavedCanvas(workspaceId, { title, blocks, sourceSessionId: sessionId });
27249
+ }
27250
+
27251
+ // src/api/routes/companion-saved-canvases.ts
27252
+ var companionSavedCanvasesController = new Hono2().get("/", zv("query", z6.object({ workspaceId: z6.string() })), async (c) => {
27253
+ const { workspaceId } = c.req.valid("query");
27254
+ return c.json(await listCompanionSavedCanvasesEntry(workspaceId));
27255
+ }).delete("/:id", zv("param", z6.object({ id: z6.string() })), async (c) => {
27256
+ const { id } = c.req.valid("param");
27257
+ await deleteCompanionSavedCanvasEntry(id);
27258
+ return c.json({ ok: true });
27259
+ });
27260
+
27261
+ // src/api/routes/companion-sessions.ts
27262
+ init_api_contract();
27263
+ import { z as z7 } from "zod";
27264
+
27265
+ // src/api/services/companion-canvases-service.ts
27266
+ var createCompanionCanvasEntry = async (sessionId, workspaceId, blocks) => {
27267
+ return createCompanionCanvas(sessionId, workspaceId, blocks);
27268
+ };
27269
+ var listCompanionCanvasesEntry = async (sessionId) => {
27270
+ return { canvases: listCompanionCanvases(sessionId) };
27271
+ };
27272
+
27273
+ // src/api/services/companion-diff-service.ts
27274
+ import { spawnSync as spawnSync7 } from "node:child_process";
27275
+ import { existsSync as existsSync11 } from "node:fs";
27276
+ var MAX_BUFFER = 4 * 1024 * 1024;
27277
+ var getCompanionDiffService = async (id) => {
27278
+ const session = getCompanionSession(id);
27279
+ if (!session) throw NotFoundError("Companion session");
27280
+ if (!session.worktreePath || !existsSync11(session.worktreePath)) {
27281
+ return { diff: null, error: "No worktree \u2014 agent has not started yet" };
27282
+ }
27283
+ return buildWorktreeDiff(session.worktreePath, session.baseRef);
27284
+ };
27285
+ var getCompanionCommitsService = async (id) => {
27286
+ const session = getCompanionSession(id);
27287
+ if (!session) throw NotFoundError("Companion session");
27288
+ if (!session.worktreePath || !existsSync11(session.worktreePath)) return { commits: [] };
27289
+ const result = spawnSync7("git", ["log", "--pretty=format:%H%x00%h%x00%s%x00%an%x00%ai", `${session.baseRef}..HEAD`], {
27290
+ cwd: session.worktreePath,
27291
+ encoding: "utf-8"
27292
+ });
27293
+ if (result.status !== 0 || !result.stdout?.trim()) return { commits: [] };
27294
+ const commits = result.stdout.trim().split("\n").filter(Boolean).map((line) => {
27295
+ const [hash = "", shortHash = "", message = "", author = "", date2 = ""] = line.split("\0");
27296
+ return { hash, shortHash, message, author, date: date2 };
27297
+ });
27298
+ return { commits };
27299
+ };
27300
+ var getCompanionDiffForCommitService = async (id, commitHash) => {
27301
+ const session = getCompanionSession(id);
27302
+ if (!session) throw NotFoundError("Companion session");
27303
+ if (!/^[0-9a-f]{4,64}$/i.test(commitHash)) return { diff: null, error: "Invalid commit hash" };
27304
+ if (!session.worktreePath || !existsSync11(session.worktreePath)) return { diff: null, error: "No worktree" };
27305
+ const result = spawnSync7("git", ["show", commitHash, "--format=", "--patch", "--no-color", "-U3"], {
27306
+ cwd: session.worktreePath,
27307
+ encoding: "utf-8",
27308
+ maxBuffer: MAX_BUFFER
27309
+ });
27310
+ if (result.status !== 0) return { diff: null, error: result.stderr?.trim() || "git show failed" };
27311
+ return { diff: result.stdout, error: null };
27312
+ };
27313
+
27314
+ // src/api/services/companion-merge-service.ts
27315
+ init_workspace_state();
27316
+ async function commitAndMergeCompanionService(id, repoPath, commitMessage, scheduler) {
27317
+ const session = getCompanionSession(id);
27318
+ if (!session) throw NotFoundError("Companion session");
27319
+ if (!session.useWorktree || !session.branchName) {
27320
+ throw BadRequestError("This session works directly in the main repo \u2014 there's nothing to merge");
27321
+ }
27322
+ if (!session.worktreePath) throw BadRequestError("Session has no worktree to merge");
27323
+ await scheduler?.stopCompanionAgent(id);
27324
+ const dirty = await isWorktreeDirty(session.worktreePath);
27325
+ if (dirty) {
27326
+ if (!commitMessage) return { status: "needs_commit" };
27327
+ await commitWorktree(session.worktreePath, commitMessage);
27328
+ }
27329
+ const result = attemptMerge(repoPath, id, session.branchName);
27330
+ setCompanionSessionWorktreePath(id, null);
27331
+ if (result.dirtyBase) {
27332
+ setCompanionSessionStatus(id, "stopped");
27333
+ return { status: "dirty_base" };
27334
+ }
27335
+ if (!result.ok) {
27336
+ abortMerge(repoPath);
27337
+ setCompanionSessionStatus(id, "stopped");
27338
+ return { status: "conflict", conflictedFiles: result.conflictedFiles };
27339
+ }
27340
+ setCompanionSessionStatus(id, "merged");
27341
+ return { status: "merged" };
27342
+ }
27343
+ async function commitAndPRCompanionService(id, workspaceId, commitMessage, title, description, baseRefOverride) {
27344
+ const session = getCompanionSession(id);
27345
+ if (!session) throw NotFoundError("Companion session");
27346
+ if (!session.worktreePath) throw BadRequestError("Session has no worktree");
27347
+ const projectConfig = await loadProjectConfig(workspaceId);
27348
+ const token = projectConfig.secrets?.find((s2) => s2.key === "GITHUB_TOKEN")?.value;
27349
+ if (!token) return { status: "no_token" };
27350
+ const dirty = await isWorktreeDirty(session.worktreePath);
27351
+ if (dirty) {
27352
+ if (!commitMessage) return { status: "needs_commit" };
27353
+ await commitWorktree(session.worktreePath, commitMessage);
27354
+ }
27355
+ const branch = session.branchName ?? getCurrentBranch(session.worktreePath);
27356
+ if (!branch) throw BadRequestError("Could not determine the current branch to push");
27357
+ await pushBranch(session.worktreePath, branch);
27358
+ const prUrl = await createGithubPR(
27359
+ session.worktreePath,
27360
+ title,
27361
+ description,
27362
+ baseRefOverride || session.baseRef,
27363
+ token
27364
+ );
27365
+ return { status: "pr_created", prUrl };
27366
+ }
27367
+
27368
+ // src/api/services/companion-service.ts
27369
+ init_api_contract();
27370
+ init_workspace_state();
27371
+ async function listCompanionSessionsEntry(workspaceId) {
27372
+ return listCompanionSessions(workspaceId);
27373
+ }
27374
+ function getCompanionSessionEntry(id) {
27375
+ return getCompanionSession(id);
27376
+ }
27377
+ async function createCompanionSessionEntry(workspaceId, repoPath, req, scheduler) {
27378
+ const useWorktree = req.useWorktree;
27379
+ const branchName = req.branchName?.trim() || null;
27380
+ if (useWorktree && !branchName) {
27381
+ throw BadRequestError("Branch name is required when using an isolated worktree");
27382
+ }
27383
+ const projectConfig = await loadProjectConfig(workspaceId);
27384
+ const workflow = req.workflowId ? projectConfig.workflows.find((w2) => w2.id === req.workflowId) : void 0;
27385
+ const devSlot = workflow?.slots.find((s2) => s2.type === "dev");
27386
+ const seedPrompt = devSlot ? resolvePromptText(devSlot.prompt, repoPath) : "";
27387
+ const suggestedPair = devSlot?.pairs[0];
27388
+ const model = req.model ?? (suggestedPair ? { agentId: suggestedPair.binary, model: suggestedPair.model, effort: suggestedPair.effort } : DEFAULT_AGENT_MODEL_CHOICE);
27389
+ const savedCanvas = req.savedCanvasId ? getCompanionSavedCanvas(req.savedCanvasId) : null;
27390
+ const name = req.name?.trim() || savedCanvas?.title || (useWorktree ? branchName : "Main repo session");
27391
+ const session = createCompanionSession(workspaceId, {
27392
+ name,
27393
+ useWorktree,
27394
+ baseRef: req.baseRef,
27395
+ branchName: useWorktree ? branchName : null,
27396
+ workflowId: workflow?.id ?? null,
27397
+ seedPrompt,
27398
+ agentId: model.agentId ?? "claude",
27399
+ model: model.model ?? null,
27400
+ effort: model.effort ?? null,
27401
+ savedCanvasId: savedCanvas?.id ?? null
27402
+ });
27403
+ if (savedCanvas) createCompanionCanvas(session.id, workspaceId, savedCanvas.blocks);
27404
+ await scheduler.startCompanionAgent(session);
27405
+ return getCompanionSession(session.id) ?? session;
27406
+ }
27407
+ async function stopCompanionSessionEntry(id, scheduler) {
27408
+ if (!getCompanionSession(id)) throw NotFoundError("Companion session");
27409
+ await scheduler?.stopCompanionAgent(id);
27410
+ }
27411
+ async function discardCompanionSessionEntry(id, repoPath, scheduler) {
27412
+ const session = getCompanionSession(id);
27413
+ if (!session) throw NotFoundError("Companion session");
27414
+ await scheduler?.stopCompanionAgent(id);
27415
+ if (session.useWorktree && session.worktreePath) {
27416
+ await removeWorktreeAsync(id, repoPath, session.branchName ?? void 0);
27417
+ }
27418
+ deleteCompanionSession(id);
27419
+ }
27420
+
27421
+ // src/api/routes/companion-sessions.ts
27422
+ var companionSessionsController = new Hono2().get("/", zv("query", z7.object({ workspaceId: z7.string() })), async (c) => {
27423
+ const { workspaceId } = c.req.valid("query");
27424
+ return c.json(await listCompanionSessionsEntry(workspaceId));
27425
+ }).get("/:id", zv("param", z7.object({ id: z7.string() })), async (c) => {
27426
+ const { id } = c.req.valid("param");
27427
+ const session = getCompanionSessionEntry(id);
27428
+ if (!session) throw NotFoundError("Companion session");
27429
+ return c.json(session);
27430
+ }).post("/", zv("json", companionSessionCreateRequestSchema.extend({ workspaceId: z7.string() })), async (c) => {
27431
+ const ctx = c.var.ctx;
27432
+ const { workspaceId, ...req } = c.req.valid("json");
27433
+ const ws = await ctx.ensureWorkspace(workspaceId);
27434
+ const scheduler = ctx.getScheduler(workspaceId);
27435
+ if (!scheduler) throw NotFoundError("Workspace");
27436
+ const session = await createCompanionSessionEntry(workspaceId, ws.repoPath, req, scheduler);
27437
+ return c.json(session);
27438
+ }).delete(
27439
+ "/:id",
27440
+ zv("param", z7.object({ id: z7.string() })),
27441
+ zv("query", z7.object({ workspaceId: z7.string() })),
27442
+ async (c) => {
27443
+ const ctx = c.var.ctx;
27444
+ const { id } = c.req.valid("param");
27445
+ const { workspaceId } = c.req.valid("query");
27446
+ await stopCompanionSessionEntry(id, ctx.getScheduler(workspaceId));
27447
+ return c.json({ ok: true });
27448
+ }
27449
+ ).post(
27450
+ "/:id/discard",
27451
+ zv("param", z7.object({ id: z7.string() })),
27452
+ zv("json", z7.object({ workspaceId: z7.string() })),
27453
+ async (c) => {
27454
+ const ctx = c.var.ctx;
27455
+ const { id } = c.req.valid("param");
27456
+ const { workspaceId } = c.req.valid("json");
27457
+ const ws = await ctx.ensureWorkspace(workspaceId);
27458
+ await discardCompanionSessionEntry(id, ws.repoPath, ctx.getScheduler(workspaceId));
27459
+ return c.json({ ok: true });
27460
+ }
27461
+ ).get("/:id/diff", zv("param", z7.object({ id: z7.string() })), async (c) => {
27462
+ const { id } = c.req.valid("param");
27463
+ return c.json(await getCompanionDiffService(id));
27464
+ }).get("/:id/commits", zv("param", z7.object({ id: z7.string() })), async (c) => {
27465
+ const { id } = c.req.valid("param");
27466
+ return c.json(await getCompanionCommitsService(id));
27467
+ }).get(
27468
+ "/:id/diff-for-commit",
27469
+ zv("param", z7.object({ id: z7.string() })),
27470
+ zv("query", z7.object({ commitHash: z7.string() })),
27471
+ async (c) => {
27472
+ const { id } = c.req.valid("param");
27473
+ const { commitHash } = c.req.valid("query");
27474
+ return c.json(await getCompanionDiffForCommitService(id, commitHash));
27475
+ }
27476
+ ).post(
27477
+ "/:id/commit-and-merge",
27478
+ zv("param", z7.object({ id: z7.string() })),
27479
+ zv("json", z7.object({ workspaceId: z7.string(), commitMessage: z7.string().optional() })),
27480
+ async (c) => {
27481
+ const ctx = c.var.ctx;
27482
+ const { id } = c.req.valid("param");
27483
+ const { workspaceId, commitMessage } = c.req.valid("json");
27484
+ const ws = await ctx.ensureWorkspace(workspaceId);
27485
+ const result = await commitAndMergeCompanionService(
27486
+ id,
27487
+ ws.repoPath,
27488
+ commitMessage,
27489
+ ctx.getScheduler(workspaceId)
27490
+ );
27491
+ return c.json(result);
27492
+ }
27493
+ ).post(
27494
+ "/:id/commit-and-pr",
27495
+ zv("param", z7.object({ id: z7.string() })),
27496
+ zv(
27497
+ "json",
27498
+ z7.object({
27499
+ workspaceId: z7.string(),
27500
+ commitMessage: z7.string().optional(),
27501
+ title: z7.string(),
27502
+ description: z7.string(),
27503
+ baseRef: z7.string().optional()
27504
+ })
27505
+ ),
27506
+ async (c) => {
27507
+ const { id } = c.req.valid("param");
27508
+ const { workspaceId, commitMessage, title, description, baseRef } = c.req.valid("json");
27509
+ const result = await commitAndPRCompanionService(id, workspaceId, commitMessage, title, description, baseRef);
27510
+ return c.json(result);
27511
+ }
27512
+ ).post(
27513
+ "/:id/canvas",
27514
+ zv("param", z7.object({ id: z7.string() })),
27515
+ zv("json", z7.object({ workspaceId: z7.string(), blocks: z7.array(canvasBlockSchema) })),
27516
+ async (c) => {
27517
+ const ctx = c.var.ctx;
27518
+ const { id } = c.req.valid("param");
27519
+ const { workspaceId, blocks } = c.req.valid("json");
27520
+ const canvas = await createCompanionCanvasEntry(id, workspaceId, blocks);
27521
+ ctx.stateHub.broadcastCompanionCanvasUpdate(workspaceId, id, canvas);
27522
+ return c.json(canvas);
27523
+ }
27524
+ ).get("/:id/canvases", zv("param", z7.object({ id: z7.string() })), async (c) => {
27525
+ const { id } = c.req.valid("param");
27526
+ return c.json(await listCompanionCanvasesEntry(id));
27527
+ }).delete("/:id/canvases", zv("param", z7.object({ id: z7.string() })), async (c) => {
27528
+ const { id } = c.req.valid("param");
27529
+ await clearCompanionCanvasesEntry(id);
27530
+ return c.json({ ok: true });
27531
+ }).post(
27532
+ "/:id/save-canvas",
27533
+ zv("param", z7.object({ id: z7.string() })),
27534
+ zv("json", z7.object({ workspaceId: z7.string(), title: z7.string(), blocks: z7.array(canvasBlockSchema) })),
27535
+ async (c) => {
27536
+ const { id } = c.req.valid("param");
27537
+ const { workspaceId, title, blocks } = c.req.valid("json");
27538
+ const saved = await saveCompanionCanvasEntry(id, workspaceId, title, blocks);
27539
+ return c.json(saved);
27540
+ }
27541
+ );
27542
+
26585
27543
  // src/api/routes/config.ts
26586
27544
  init_api_contract();
26587
27545
 
@@ -26605,20 +27563,20 @@ var configController = new Hono2().get("/", async (c) => {
26605
27563
  });
26606
27564
 
26607
27565
  // src/api/routes/fs.ts
26608
- import { z as z6 } from "zod";
27566
+ import { z as z8 } from "zod";
26609
27567
 
26610
27568
  // src/api/services/fs-service.ts
26611
27569
  init_runtime_config();
26612
- import { spawnSync as spawnSync8 } from "node:child_process";
26613
- import { existsSync as existsSync12, readdirSync as readdirSync2, statSync } from "node:fs";
27570
+ import { spawnSync as spawnSync9 } from "node:child_process";
27571
+ import { existsSync as existsSync13, readdirSync as readdirSync2, statSync } from "node:fs";
26614
27572
  import { homedir as homedir4 } from "node:os";
26615
- import { dirname as dirname7, join as join19, resolve as resolve3 } from "node:path";
27573
+ import { dirname as dirname7, join as join20, resolve as resolve3 } from "node:path";
26616
27574
 
26617
27575
  // src/core/terminal-apps.ts
26618
- import { spawnSync as spawnSync7 } from "node:child_process";
26619
- import { existsSync as existsSync11 } from "node:fs";
27576
+ import { spawnSync as spawnSync8 } from "node:child_process";
27577
+ import { existsSync as existsSync12 } from "node:fs";
26620
27578
  import { homedir as homedir3 } from "node:os";
26621
- import { join as join18 } from "node:path";
27579
+ import { join as join19 } from "node:path";
26622
27580
  var MACOS_TERMINALS = [
26623
27581
  { bundle: "Terminal", label: "Terminal" },
26624
27582
  { bundle: "iTerm", label: "iTerm" },
@@ -26650,13 +27608,13 @@ function appExists(bundle) {
26650
27608
  `/Applications/${bundle}.app`,
26651
27609
  `/System/Applications/${bundle}.app`,
26652
27610
  `/System/Applications/Utilities/${bundle}.app`,
26653
- join18(homedir3(), "Applications", `${bundle}.app`)
27611
+ join19(homedir3(), "Applications", `${bundle}.app`)
26654
27612
  ];
26655
- return paths.some((p2) => existsSync11(p2));
27613
+ return paths.some((p2) => existsSync12(p2));
26656
27614
  }
26657
27615
  function binaryExists(bin) {
26658
27616
  const finder = process.platform === "win32" ? "where" : "which";
26659
- const r = spawnSync7(finder, [bin], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" });
27617
+ const r = spawnSync8(finder, [bin], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" });
26660
27618
  return r.status === 0 && r.stdout.trim().length > 0;
26661
27619
  }
26662
27620
  function listTerminalApps() {
@@ -26671,26 +27629,26 @@ function listTerminalApps() {
26671
27629
  function openTerminalAt(path2, preferredId) {
26672
27630
  if (process.platform === "darwin") {
26673
27631
  const bundle = preferredId && appExists(preferredId) ? preferredId : "Terminal";
26674
- spawnSync7("open", ["-a", bundle, path2], { stdio: "ignore" });
27632
+ spawnSync8("open", ["-a", bundle, path2], { stdio: "ignore" });
26675
27633
  return;
26676
27634
  }
26677
27635
  if (process.platform === "win32") {
26678
27636
  const id = preferredId && WINDOWS_TERMINALS.some((t) => t.id === preferredId && binaryExists(t.check)) ? preferredId : "cmd";
26679
27637
  if (id === "wt") {
26680
- spawnSync7("wt", ["-d", path2], { stdio: "ignore" });
27638
+ spawnSync8("wt", ["-d", path2], { stdio: "ignore" });
26681
27639
  } else if (id === "powershell") {
26682
- spawnSync7("cmd", ["/c", "start", "powershell", "-NoExit", "-Command", `Set-Location -Path '${path2}'`], {
27640
+ spawnSync8("cmd", ["/c", "start", "powershell", "-NoExit", "-Command", `Set-Location -Path '${path2}'`], {
26683
27641
  stdio: "ignore"
26684
27642
  });
26685
27643
  } else {
26686
- spawnSync7("cmd", ["/c", "start", "cmd", "/K", `cd /D "${path2}"`], { stdio: "ignore" });
27644
+ spawnSync8("cmd", ["/c", "start", "cmd", "/K", `cd /D "${path2}"`], { stdio: "ignore" });
26687
27645
  }
26688
27646
  return;
26689
27647
  }
26690
27648
  const bin = preferredId && binaryExists(preferredId) ? preferredId : LINUX_TERMINALS.find((t) => binaryExists(t.bin))?.bin;
26691
27649
  if (!bin) return;
26692
27650
  const args = linuxLaunchArgs(bin, path2);
26693
- spawnSync7(bin, args, { stdio: "ignore" });
27651
+ spawnSync8(bin, args, { stdio: "ignore" });
26694
27652
  }
26695
27653
  function linuxLaunchArgs(bin, path2) {
26696
27654
  switch (bin) {
@@ -26716,7 +27674,7 @@ function linuxLaunchArgs(bin, path2) {
26716
27674
  // src/api/services/fs-service.ts
26717
27675
  var openPath = (path2) => {
26718
27676
  const cmd = process.platform === "win32" ? "explorer" : process.platform === "darwin" ? "open" : "xdg-open";
26719
- spawnSync8(cmd, [path2], { stdio: "ignore" });
27677
+ spawnSync9(cmd, [path2], { stdio: "ignore" });
26720
27678
  return { ok: true };
26721
27679
  };
26722
27680
  var listTerminals = async () => listTerminalApps();
@@ -26727,15 +27685,15 @@ var openTerminal = async (path2) => {
26727
27685
  };
26728
27686
  var listDir = async (path2, includeFiles, showHidden) => {
26729
27687
  let target = resolve3(path2 || homedir4());
26730
- while (target !== dirname7(target) && !(existsSync12(target) && statSync(target).isDirectory())) {
27688
+ while (target !== dirname7(target) && !(existsSync13(target) && statSync(target).isDirectory())) {
26731
27689
  target = dirname7(target);
26732
27690
  }
26733
27691
  const parent = dirname7(target);
26734
27692
  const visible = (name) => showHidden || !name.startsWith(".");
26735
27693
  try {
26736
27694
  const entries = readdirSync2(target, { withFileTypes: true });
26737
- const dirs = entries.filter((e) => e.isDirectory() && visible(e.name)).map((e) => ({ name: e.name, path: join19(target, e.name) })).sort((a, b2) => a.name.localeCompare(b2.name));
26738
- const files = includeFiles ? entries.filter((e) => e.isFile() && visible(e.name)).map((e) => ({ name: e.name, path: join19(target, e.name) })).sort((a, b2) => a.name.localeCompare(b2.name)) : [];
27695
+ const dirs = entries.filter((e) => e.isDirectory() && visible(e.name)).map((e) => ({ name: e.name, path: join20(target, e.name) })).sort((a, b2) => a.name.localeCompare(b2.name));
27696
+ const files = includeFiles ? entries.filter((e) => e.isFile() && visible(e.name)).map((e) => ({ name: e.name, path: join20(target, e.name) })).sort((a, b2) => a.name.localeCompare(b2.name)) : [];
26739
27697
  return { current: target, parent: parent !== target ? parent : null, dirs, files };
26740
27698
  } catch {
26741
27699
  return { current: target, parent: parent !== target ? parent : null, dirs: [], files: [] };
@@ -26743,22 +27701,22 @@ var listDir = async (path2, includeFiles, showHidden) => {
26743
27701
  };
26744
27702
 
26745
27703
  // src/api/routes/fs.ts
26746
- var fsController = new Hono2().post("/open", zv("json", z6.object({ path: z6.string() })), async (c) => {
27704
+ var fsController = new Hono2().post("/open", zv("json", z8.object({ path: z8.string() })), async (c) => {
26747
27705
  const { path: path2 } = c.req.valid("json");
26748
27706
  return c.json(openPath(path2));
26749
27707
  }).get("/terminals", async (c) => {
26750
27708
  return c.json(await listTerminals());
26751
- }).post("/open-terminal", zv("json", z6.object({ path: z6.string() })), async (c) => {
27709
+ }).post("/open-terminal", zv("json", z8.object({ path: z8.string() })), async (c) => {
26752
27710
  const { path: path2 } = c.req.valid("json");
26753
27711
  return c.json(await openTerminal(path2));
26754
27712
  }).get(
26755
27713
  "/list-dir",
26756
27714
  zv(
26757
27715
  "query",
26758
- z6.object({
26759
- path: z6.string().optional().default(""),
26760
- includeFiles: z6.coerce.boolean().optional(),
26761
- showHidden: z6.coerce.boolean().optional()
27716
+ z8.object({
27717
+ path: z8.string().optional().default(""),
27718
+ includeFiles: z8.coerce.boolean().optional(),
27719
+ showHidden: z8.coerce.boolean().optional()
26762
27720
  })
26763
27721
  ),
26764
27722
  async (c) => {
@@ -26769,7 +27727,7 @@ var fsController = new Hono2().post("/open", zv("json", z6.object({ path: z6.str
26769
27727
 
26770
27728
  // src/api/routes/memory.ts
26771
27729
  init_api_contract();
26772
- import { z as z7 } from "zod";
27730
+ import { z as z9 } from "zod";
26773
27731
 
26774
27732
  // src/api/services/memory-service.ts
26775
27733
  var listMemoryEntries = async (filter) => listMemories(filter);
@@ -26801,9 +27759,9 @@ var memoryController = new Hono2().get(
26801
27759
  "/",
26802
27760
  zv(
26803
27761
  "query",
26804
- z7.object({
27762
+ z9.object({
26805
27763
  scope: memoryScopeSchema,
26806
- workspaceId: z7.string().optional(),
27764
+ workspaceId: z9.string().optional(),
26807
27765
  status: memoryStatusSchema.optional()
26808
27766
  })
26809
27767
  ),
@@ -26817,33 +27775,33 @@ var memoryController = new Hono2().get(
26817
27775
  })
26818
27776
  );
26819
27777
  }
26820
- ).get("/search", zv("query", z7.object({ query: z7.string(), workspaceId: z7.string().optional() })), async (c) => {
27778
+ ).get("/search", zv("query", z9.object({ query: z9.string(), workspaceId: z9.string().optional() })), async (c) => {
26821
27779
  const input = c.req.valid("query");
26822
27780
  return c.json(await searchMemoryEntries(input.query, input.workspaceId ?? null));
26823
- }).get("/for-card", zv("query", z7.object({ cardId: z7.string() })), async (c) => {
27781
+ }).get("/for-card", zv("query", z9.object({ cardId: z9.string() })), async (c) => {
26824
27782
  const input = c.req.valid("query");
26825
27783
  return c.json(await listMemoryEntriesForCard(input.cardId));
26826
- }).get("/tags", async (c) => c.json(await listTagsEntries())).get("/workspace-tags", zv("query", z7.object({ workspaceId: z7.string() })), async (c) => {
27784
+ }).get("/tags", async (c) => c.json(await listTagsEntries())).get("/workspace-tags", zv("query", z9.object({ workspaceId: z9.string() })), async (c) => {
26827
27785
  const { workspaceId } = c.req.valid("query");
26828
27786
  return c.json(await getWorkspaceTagsEntry(workspaceId));
26829
- }).get("/:id", zv("param", z7.object({ id: z7.string() })), async (c) => {
27787
+ }).get("/:id", zv("param", z9.object({ id: z9.string() })), async (c) => {
26830
27788
  const { id } = c.req.valid("param");
26831
27789
  return c.json(await getMemoryEntry(id));
26832
27790
  }).post(
26833
27791
  "/propose",
26834
27792
  zv(
26835
27793
  "json",
26836
- z7.object({
27794
+ z9.object({
26837
27795
  scope: memoryScopeSchema,
26838
- workspaceId: z7.string().optional(),
26839
- originWorkspaceId: z7.string().optional(),
27796
+ workspaceId: z9.string().optional(),
27797
+ originWorkspaceId: z9.string().optional(),
26840
27798
  type: memoryTypeSchema,
26841
- title: z7.string().min(1),
26842
- content: z7.string().min(1),
27799
+ title: z9.string().min(1),
27800
+ content: z9.string().min(1),
26843
27801
  sourceType: memorySourceTypeSchema.default("task_lesson"),
26844
- importance: z7.number().int().min(1).max(3).optional(),
26845
- tags: z7.array(z7.string()).optional(),
26846
- originCardId: z7.string().optional(),
27802
+ importance: z9.number().int().min(1).max(3).optional(),
27803
+ tags: z9.array(z9.string()).optional(),
27804
+ originCardId: z9.string().optional(),
26847
27805
  originAgent: runtimeMemoryOriginAgentSchema.optional()
26848
27806
  })
26849
27807
  ),
@@ -26875,12 +27833,12 @@ var memoryController = new Hono2().get(
26875
27833
  "/propose-update",
26876
27834
  zv(
26877
27835
  "json",
26878
- z7.object({
26879
- id: z7.string(),
27836
+ z9.object({
27837
+ id: z9.string(),
26880
27838
  type: memoryTypeSchema.optional(),
26881
- title: z7.string().min(1).optional(),
26882
- content: z7.string().min(1).optional(),
26883
- importance: z7.number().int().min(1).max(3).optional(),
27839
+ title: z9.string().min(1).optional(),
27840
+ content: z9.string().min(1).optional(),
27841
+ importance: z9.number().int().min(1).max(3).optional(),
26884
27842
  sourceType: memorySourceTypeSchema.default("task_lesson")
26885
27843
  })
26886
27844
  ),
@@ -26894,16 +27852,16 @@ var memoryController = new Hono2().get(
26894
27852
  "/",
26895
27853
  zv(
26896
27854
  "json",
26897
- z7.object({
27855
+ z9.object({
26898
27856
  scope: memoryScopeSchema,
26899
- workspaceId: z7.string().optional(),
26900
- originWorkspaceId: z7.string().optional(),
27857
+ workspaceId: z9.string().optional(),
27858
+ originWorkspaceId: z9.string().optional(),
26901
27859
  type: memoryTypeSchema,
26902
- title: z7.string().min(1),
26903
- content: z7.string().min(1),
26904
- importance: z7.number().int().min(1).max(3).optional(),
26905
- tags: z7.array(z7.string()).optional(),
26906
- boundWorkspaceIds: z7.array(z7.string()).optional()
27860
+ title: z9.string().min(1),
27861
+ content: z9.string().min(1),
27862
+ importance: z9.number().int().min(1).max(3).optional(),
27863
+ tags: z9.array(z9.string()).optional(),
27864
+ boundWorkspaceIds: z9.array(z9.string()).optional()
26907
27865
  })
26908
27866
  ),
26909
27867
  async (c) => {
@@ -26932,14 +27890,14 @@ var memoryController = new Hono2().get(
26932
27890
  }
26933
27891
  ).patch(
26934
27892
  "/:id",
26935
- zv("param", z7.object({ id: z7.string() })),
27893
+ zv("param", z9.object({ id: z9.string() })),
26936
27894
  zv(
26937
27895
  "json",
26938
- z7.object({
27896
+ z9.object({
26939
27897
  type: memoryTypeSchema.optional(),
26940
- title: z7.string().min(1).optional(),
26941
- content: z7.string().min(1).optional(),
26942
- importance: z7.number().int().min(1).max(3).optional()
27898
+ title: z9.string().min(1).optional(),
27899
+ content: z9.string().min(1).optional(),
27900
+ importance: z9.number().int().min(1).max(3).optional()
26943
27901
  })
26944
27902
  ),
26945
27903
  async (c) => {
@@ -26948,19 +27906,19 @@ var memoryController = new Hono2().get(
26948
27906
  if (!updated) throw NotFoundError("Memory");
26949
27907
  return c.json(updated);
26950
27908
  }
26951
- ).post("/:id/approve", zv("param", z7.object({ id: z7.string() })), async (c) => {
27909
+ ).post("/:id/approve", zv("param", z9.object({ id: z9.string() })), async (c) => {
26952
27910
  const { id } = c.req.valid("param");
26953
27911
  const approved = await approveMemoryEntry(id);
26954
27912
  if (!approved) throw NotFoundError("Memory");
26955
27913
  return c.json(approved);
26956
- }).delete("/:id", zv("param", z7.object({ id: z7.string() })), async (c) => {
27914
+ }).delete("/:id", zv("param", z9.object({ id: z9.string() })), async (c) => {
26957
27915
  const { id } = c.req.valid("param");
26958
27916
  await removeMemoryEntry(id);
26959
27917
  return c.json({ ok: true });
26960
27918
  }).patch(
26961
27919
  "/:id/tags",
26962
- zv("param", z7.object({ id: z7.string() })),
26963
- zv("json", z7.object({ tags: z7.array(z7.string()) })),
27920
+ zv("param", z9.object({ id: z9.string() })),
27921
+ zv("json", z9.object({ tags: z9.array(z9.string()) })),
26964
27922
  async (c) => {
26965
27923
  const { id } = c.req.valid("param");
26966
27924
  const { tags } = c.req.valid("json");
@@ -26970,8 +27928,8 @@ var memoryController = new Hono2().get(
26970
27928
  }
26971
27929
  ).patch(
26972
27930
  "/:id/bindings",
26973
- zv("param", z7.object({ id: z7.string() })),
26974
- zv("json", z7.object({ workspaceIds: z7.array(z7.string()) })),
27931
+ zv("param", z9.object({ id: z9.string() })),
27932
+ zv("json", z9.object({ workspaceIds: z9.array(z9.string()) })),
26975
27933
  async (c) => {
26976
27934
  const { id } = c.req.valid("param");
26977
27935
  const { workspaceIds } = c.req.valid("json");
@@ -26979,7 +27937,7 @@ var memoryController = new Hono2().get(
26979
27937
  if (!updated) throw NotFoundError("Memory");
26980
27938
  return c.json(updated);
26981
27939
  }
26982
- ).put("/workspace-tags", zv("json", z7.object({ workspaceId: z7.string(), tags: z7.array(z7.string()) })), async (c) => {
27940
+ ).put("/workspace-tags", zv("json", z9.object({ workspaceId: z9.string(), tags: z9.array(z9.string()) })), async (c) => {
26983
27941
  const { workspaceId, tags } = c.req.valid("json");
26984
27942
  await setWorkspaceTagsEntry(workspaceId, tags);
26985
27943
  return c.json({ ok: true });
@@ -26987,7 +27945,7 @@ var memoryController = new Hono2().get(
26987
27945
 
26988
27946
  // src/api/routes/project-config.ts
26989
27947
  init_api_contract();
26990
- import { z as z8 } from "zod";
27948
+ import { z as z10 } from "zod";
26991
27949
 
26992
27950
  // src/api/services/project-config-service.ts
26993
27951
  init_workspace_state();
@@ -27007,21 +27965,21 @@ var setSystemPrompt = async (workspaceId, prompt) => {
27007
27965
  };
27008
27966
 
27009
27967
  // src/api/routes/project-config.ts
27010
- var projectConfigController = new Hono2().get("/", zv("query", z8.object({ workspaceId: z8.string() })), async (c) => {
27968
+ var projectConfigController = new Hono2().get("/", zv("query", z10.object({ workspaceId: z10.string() })), async (c) => {
27011
27969
  return c.json(await getProjectConfig(c.req.valid("query").workspaceId));
27012
- }).put("/", zv("json", z8.object({ workspaceId: z8.string(), config: runtimeProjectConfigSchema })), async (c) => {
27970
+ }).put("/", zv("json", z10.object({ workspaceId: z10.string(), config: runtimeProjectConfigSchema })), async (c) => {
27013
27971
  const ctx = c.var.ctx;
27014
27972
  const { workspaceId, config } = c.req.valid("json");
27015
27973
  await saveProjectConfig2(workspaceId, config);
27016
27974
  ctx.stateHub.broadcastWorkspaceUpdate(workspaceId);
27017
27975
  return c.json({ ok: true });
27018
- }).post("/git-instructions", zv("json", z8.object({ workspaceId: z8.string(), instructions: z8.string() })), async (c) => {
27976
+ }).post("/git-instructions", zv("json", z10.object({ workspaceId: z10.string(), instructions: z10.string() })), async (c) => {
27019
27977
  const ctx = c.var.ctx;
27020
27978
  const { workspaceId, instructions } = c.req.valid("json");
27021
27979
  const { cleared } = await setGitInstructions(workspaceId, instructions);
27022
27980
  ctx.stateHub.broadcastWorkspaceUpdate(workspaceId);
27023
27981
  return c.json({ ok: true, cleared });
27024
- }).post("/system-prompt", zv("json", z8.object({ workspaceId: z8.string(), prompt: z8.string() })), async (c) => {
27982
+ }).post("/system-prompt", zv("json", z10.object({ workspaceId: z10.string(), prompt: z10.string() })), async (c) => {
27025
27983
  const ctx = c.var.ctx;
27026
27984
  const { workspaceId, prompt } = c.req.valid("json");
27027
27985
  const { cleared } = await setSystemPrompt(workspaceId, prompt);
@@ -27031,12 +27989,12 @@ var projectConfigController = new Hono2().get("/", zv("query", z8.object({ works
27031
27989
 
27032
27990
  // src/api/routes/projects.ts
27033
27991
  init_api_contract();
27034
- import { z as z9 } from "zod";
27992
+ import { z as z11 } from "zod";
27035
27993
 
27036
27994
  // src/api/services/projects-service.ts
27037
27995
  init_runtime_config();
27038
27996
  init_api_contract();
27039
- import { spawnSync as spawnSync9 } from "node:child_process";
27997
+ import { spawnSync as spawnSync10 } from "node:child_process";
27040
27998
  init_logger();
27041
27999
 
27042
28000
  // src/state/projects-layout.ts
@@ -27119,7 +28077,7 @@ var checkProjectPath = async (repoPath) => {
27119
28077
  branches: []
27120
28078
  };
27121
28079
  }
27122
- const r = spawnSync9("git", ["rev-parse", "--git-dir"], {
28080
+ const r = spawnSync10("git", ["rev-parse", "--git-dir"], {
27123
28081
  cwd: repoPath,
27124
28082
  encoding: "utf-8",
27125
28083
  stdio: ["ignore", "pipe", "pipe"]
@@ -27135,12 +28093,12 @@ var checkProjectPath = async (repoPath) => {
27135
28093
  remote: null,
27136
28094
  branches: []
27137
28095
  };
27138
- const branchR = spawnSync9("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
28096
+ const branchR = spawnSync10("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
27139
28097
  cwd: repoPath,
27140
28098
  encoding: "utf-8",
27141
28099
  stdio: ["ignore", "pipe", "pipe"]
27142
28100
  });
27143
- const remoteR = spawnSync9("git", ["remote", "get-url", "origin"], {
28101
+ const remoteR = spawnSync10("git", ["remote", "get-url", "origin"], {
27144
28102
  cwd: repoPath,
27145
28103
  encoding: "utf-8",
27146
28104
  stdio: ["ignore", "pipe", "pipe"]
@@ -27159,7 +28117,7 @@ var addProject = async (repoPath, initialConfig) => {
27159
28117
  } catch {
27160
28118
  throw BadRequestError(`Path does not exist: ${repoPath}`);
27161
28119
  }
27162
- const r = spawnSync9("git", ["rev-parse", "--git-dir"], {
28120
+ const r = spawnSync10("git", ["rev-parse", "--git-dir"], {
27163
28121
  cwd: repoPath,
27164
28122
  encoding: "utf-8",
27165
28123
  stdio: ["ignore", "pipe", "pipe"]
@@ -27191,7 +28149,7 @@ var removeProject = async (workspaceId) => {
27191
28149
  const cards = boardCards;
27192
28150
  enqueueCleanup2(async () => {
27193
28151
  const { rm: rm3 } = await import("node:fs/promises");
27194
- const { join: join23 } = await import("node:path");
28152
+ const { join: join24 } = await import("node:path");
27195
28153
  for (const [cardId, card] of Object.entries(cards)) {
27196
28154
  if (resolveWorktreeOwnerId(cardId, cards) === cardId) {
27197
28155
  await removeWorktreeAsync(cardId, repoPath, card.branchName).catch((err) => {
@@ -27199,11 +28157,11 @@ var removeProject = async (workspaceId) => {
27199
28157
  });
27200
28158
  }
27201
28159
  }
27202
- await rm3(join23(WORKSPACES_DIR, workspaceId), { recursive: true, force: true }).catch((err) => {
28160
+ await rm3(join24(WORKSPACES_DIR, workspaceId), { recursive: true, force: true }).catch((err) => {
27203
28161
  logger.warn(`[cleanup:project:${workspaceId}] workspace dir failed: ${String(err)}`);
27204
28162
  });
27205
28163
  for (const cardId of Object.keys(cards)) {
27206
- await rm3(join23(ATTACHMENTS_DIR, cardId), { recursive: true, force: true }).catch(() => {
28164
+ await rm3(join24(ATTACHMENTS_DIR, cardId), { recursive: true, force: true }).catch(() => {
27207
28165
  });
27208
28166
  }
27209
28167
  logger.info(`[cleanup:project:${workspaceId}] done`);
@@ -27217,14 +28175,14 @@ var projectsController = new Hono2().get("/", async (c) => {
27217
28175
  return c.json(await listProjects());
27218
28176
  }).get("/layout", (c) => {
27219
28177
  return c.json(getProjectsLayout());
27220
- }).get("/check-path", zv("query", z9.object({ repoPath: z9.string() })), async (c) => {
28178
+ }).get("/check-path", zv("query", z11.object({ repoPath: z11.string() })), async (c) => {
27221
28179
  return c.json(await checkProjectPath(c.req.valid("query").repoPath));
27222
28180
  }).post(
27223
28181
  "/",
27224
28182
  zv(
27225
28183
  "json",
27226
- z9.object({
27227
- repoPath: z9.string().min(1),
28184
+ z11.object({
28185
+ repoPath: z11.string().min(1),
27228
28186
  initialConfig: runtimeProjectConfigSchema.partial().optional()
27229
28187
  })
27230
28188
  ),
@@ -27251,7 +28209,7 @@ var projectsController = new Hono2().get("/", async (c) => {
27251
28209
 
27252
28210
  // src/api/routes/recurring-agents.ts
27253
28211
  init_api_contract();
27254
- import { z as z10 } from "zod";
28212
+ import { z as z12 } from "zod";
27255
28213
 
27256
28214
  // src/api/services/recurring-agents-service.ts
27257
28215
  function listRecurringAgentsEntry(workspaceId) {
@@ -27276,20 +28234,20 @@ function setRecurringAgentJournalEntry(id, journal) {
27276
28234
  }
27277
28235
 
27278
28236
  // src/api/routes/recurring-agents.ts
27279
- var recurringAgentsController = new Hono2().get("/", zv("query", z10.object({ workspaceId: z10.string() })), async (c) => {
28237
+ var recurringAgentsController = new Hono2().get("/", zv("query", z12.object({ workspaceId: z12.string() })), async (c) => {
27280
28238
  const { workspaceId } = c.req.valid("query");
27281
28239
  return c.json(listRecurringAgentsEntry(workspaceId));
27282
- }).get("/:id", zv("param", z10.object({ id: z10.string() })), async (c) => {
28240
+ }).get("/:id", zv("param", z12.object({ id: z12.string() })), async (c) => {
27283
28241
  const { id } = c.req.valid("param");
27284
28242
  const agent = getRecurringAgentEntry(id);
27285
28243
  if (!agent) throw NotFoundError("Recurring agent");
27286
28244
  return c.json(agent);
27287
- }).post("/", zv("json", recurringAgentCreateRequestSchema.extend({ workspaceId: z10.string() })), async (c) => {
28245
+ }).post("/", zv("json", recurringAgentCreateRequestSchema.extend({ workspaceId: z12.string() })), async (c) => {
27288
28246
  const { workspaceId, ...req } = c.req.valid("json");
27289
28247
  return c.json(createRecurringAgentEntry(workspaceId, req));
27290
28248
  }).patch(
27291
28249
  "/:id",
27292
- zv("param", z10.object({ id: z10.string() })),
28250
+ zv("param", z12.object({ id: z12.string() })),
27293
28251
  zv("json", recurringAgentUpdateRequestSchema.omit({ id: true })),
27294
28252
  async (c) => {
27295
28253
  const { id } = c.req.valid("param");
@@ -27299,8 +28257,8 @@ var recurringAgentsController = new Hono2().get("/", zv("query", z10.object({ wo
27299
28257
  }
27300
28258
  ).post(
27301
28259
  "/:id/journal",
27302
- zv("param", z10.object({ id: z10.string() })),
27303
- zv("json", z10.object({ journal: z10.string() })),
28260
+ zv("param", z12.object({ id: z12.string() })),
28261
+ zv("json", z12.object({ journal: z12.string() })),
27304
28262
  async (c) => {
27305
28263
  const { id } = c.req.valid("param");
27306
28264
  const { journal } = c.req.valid("json");
@@ -27310,8 +28268,8 @@ var recurringAgentsController = new Hono2().get("/", zv("query", z10.object({ wo
27310
28268
  }
27311
28269
  ).post(
27312
28270
  "/:id/run",
27313
- zv("param", z10.object({ id: z10.string() })),
27314
- zv("query", z10.object({ workspaceId: z10.string() })),
28271
+ zv("param", z12.object({ id: z12.string() })),
28272
+ zv("query", z12.object({ workspaceId: z12.string() })),
27315
28273
  async (c) => {
27316
28274
  const { id } = c.req.valid("param");
27317
28275
  const { workspaceId } = c.req.valid("query");
@@ -27320,14 +28278,14 @@ var recurringAgentsController = new Hono2().get("/", zv("query", z10.object({ wo
27320
28278
  const started = await scheduler?.runNow(id) ?? false;
27321
28279
  return c.json({ started });
27322
28280
  }
27323
- ).delete("/:id", zv("param", z10.object({ id: z10.string() })), async (c) => {
28281
+ ).delete("/:id", zv("param", z12.object({ id: z12.string() })), async (c) => {
27324
28282
  const { id } = c.req.valid("param");
27325
28283
  deleteRecurringAgentEntry(id);
27326
28284
  return c.json({ ok: true });
27327
28285
  });
27328
28286
 
27329
28287
  // src/api/routes/run.ts
27330
- import { z as z11 } from "zod";
28288
+ import { z as z13 } from "zod";
27331
28289
 
27332
28290
  // src/api/services/run-service.ts
27333
28291
  init_workspace_state();
@@ -27343,15 +28301,19 @@ var resolveCardCwd = async (workspaceId, cardId, repoPath) => {
27343
28301
  if (!card) return null;
27344
28302
  return card.worktreePath ?? repoPath;
27345
28303
  };
28304
+ var resolveCompanionSessionCwd = (sessionId) => {
28305
+ const session = getCompanionSession(sessionId);
28306
+ return session?.worktreePath ?? null;
28307
+ };
27346
28308
 
27347
28309
  // src/api/routes/run.ts
27348
- var runController = new Hono2().get("/status", zv("query", z11.object({ workspaceId: z11.string() })), (c) => {
28310
+ var runController = new Hono2().get("/status", zv("query", z13.object({ workspaceId: z13.string() })), (c) => {
27349
28311
  const ctx = c.var.ctx;
27350
28312
  const { workspaceId } = c.req.valid("query");
27351
28313
  const session = ctx.getRunSession(workspaceId);
27352
28314
  if (!session) return c.json({ cardId: null, status: "stopped", errorMessage: void 0 });
27353
28315
  return c.json({ cardId: session.cardId, status: session.status, errorMessage: session.errorMessage });
27354
- }).post("/start", zv("json", z11.object({ workspaceId: z11.string(), cardId: z11.string() })), async (c) => {
28316
+ }).post("/start", zv("json", z13.object({ workspaceId: z13.string(), cardId: z13.string() })), async (c) => {
27355
28317
  const ctx = c.var.ctx;
27356
28318
  const { workspaceId, cardId } = c.req.valid("json");
27357
28319
  const ws = await ctx.ensureWorkspace(workspaceId);
@@ -27363,7 +28325,18 @@ var runController = new Hono2().get("/status", zv("query", z11.object({ workspac
27363
28325
  if (cwd === null) throw NotFoundError("Card");
27364
28326
  ctx.startRun(workspaceId, cardId, command, cwd);
27365
28327
  return c.json({ ok: true });
27366
- }).post("/start-base", zv("json", z11.object({ workspaceId: z11.string() })), async (c) => {
28328
+ }).post("/start-companion", zv("json", z13.object({ workspaceId: z13.string(), sessionId: z13.string() })), async (c) => {
28329
+ const ctx = c.var.ctx;
28330
+ const { workspaceId, sessionId } = c.req.valid("json");
28331
+ const command = await resolveStartCommand(workspaceId);
28332
+ if (!command) {
28333
+ throw PreconditionFailedError("No start command configured. Add one in Settings \u2192 Environment.");
28334
+ }
28335
+ const cwd = resolveCompanionSessionCwd(sessionId);
28336
+ if (cwd === null) throw NotFoundError("Companion session");
28337
+ ctx.startRun(workspaceId, sessionId, command, cwd);
28338
+ return c.json({ ok: true });
28339
+ }).post("/start-base", zv("json", z13.object({ workspaceId: z13.string() })), async (c) => {
27367
28340
  const ctx = c.var.ctx;
27368
28341
  const { workspaceId } = c.req.valid("json");
27369
28342
  const ws = await ctx.ensureWorkspace(workspaceId);
@@ -27373,7 +28346,7 @@ var runController = new Hono2().get("/status", zv("query", z11.object({ workspac
27373
28346
  }
27374
28347
  ctx.startRun(workspaceId, null, command, ws.repoPath);
27375
28348
  return c.json({ ok: true });
27376
- }).post("/stop", zv("json", z11.object({ workspaceId: z11.string() })), (c) => {
28349
+ }).post("/stop", zv("json", z13.object({ workspaceId: z13.string() })), (c) => {
27377
28350
  const ctx = c.var.ctx;
27378
28351
  const { workspaceId } = c.req.valid("json");
27379
28352
  ctx.stopRun(workspaceId);
@@ -27381,7 +28354,7 @@ var runController = new Hono2().get("/status", zv("query", z11.object({ workspac
27381
28354
  });
27382
28355
 
27383
28356
  // src/api/routes/slack.ts
27384
- import { z as z12 } from "zod";
28357
+ import { z as z14 } from "zod";
27385
28358
 
27386
28359
  // src/api/services/slack-service.ts
27387
28360
  init_runtime_config();
@@ -27428,20 +28401,20 @@ var createApp = async (appConfigToken, publicUrl, botName) => {
27428
28401
  var slackController = new Hono2().post("/resetApp", async (c) => {
27429
28402
  await resetApp();
27430
28403
  return c.json({ ok: true });
27431
- }).post("/updateSigningSecret", zv("json", z12.object({ signingSecret: z12.string().min(1) })), async (c) => {
28404
+ }).post("/updateSigningSecret", zv("json", z14.object({ signingSecret: z14.string().min(1) })), async (c) => {
27432
28405
  await updateSigningSecret(c.req.valid("json").signingSecret);
27433
28406
  return c.json({ ok: true });
27434
28407
  }).post(
27435
28408
  "/importCredentials",
27436
28409
  zv(
27437
28410
  "json",
27438
- z12.object({
27439
- slackAppId: z12.string(),
27440
- slackClientId: z12.string(),
27441
- slackClientSecret: z12.string(),
27442
- slackSigningSecret: z12.string(),
27443
- slackOauthAuthorizeUrl: z12.string(),
27444
- slackPublicUrl: z12.string()
28411
+ z14.object({
28412
+ slackAppId: z14.string(),
28413
+ slackClientId: z14.string(),
28414
+ slackClientSecret: z14.string(),
28415
+ slackSigningSecret: z14.string(),
28416
+ slackOauthAuthorizeUrl: z14.string(),
28417
+ slackPublicUrl: z14.string()
27445
28418
  })
27446
28419
  ),
27447
28420
  async (c) => {
@@ -27450,7 +28423,7 @@ var slackController = new Hono2().post("/resetApp", async (c) => {
27450
28423
  }
27451
28424
  ).post(
27452
28425
  "/createApp",
27453
- zv("json", z12.object({ appConfigToken: z12.string(), publicUrl: z12.string(), botName: z12.string().default("Whipped") })),
28426
+ zv("json", z14.object({ appConfigToken: z14.string(), publicUrl: z14.string(), botName: z14.string().default("Whipped") })),
27454
28427
  async (c) => {
27455
28428
  const { appConfigToken, publicUrl, botName } = c.req.valid("json");
27456
28429
  return c.json(await createApp(appConfigToken, publicUrl, botName));
@@ -27458,27 +28431,27 @@ var slackController = new Hono2().post("/resetApp", async (c) => {
27458
28431
  );
27459
28432
 
27460
28433
  // src/api/routes/terminal.ts
27461
- import { z as z13 } from "zod";
28434
+ import { z as z15 } from "zod";
27462
28435
 
27463
28436
  // src/api/services/terminal-service.ts
27464
28437
  var toBufferResponse = (buffer) => ({ data: buffer ?? "" });
27465
28438
 
27466
28439
  // src/api/routes/terminal.ts
27467
- var terminalController = new Hono2().get("/buffer", zv("query", z13.object({ workspaceId: z13.string(), taskId: z13.string() })), (c) => {
28440
+ var terminalController = new Hono2().get("/buffer", zv("query", z15.object({ workspaceId: z15.string(), taskId: z15.string() })), (c) => {
27468
28441
  const ctx = c.var.ctx;
27469
28442
  const { workspaceId, taskId } = c.req.valid("query");
27470
28443
  const buf = ctx.getScheduler(workspaceId)?.getOutputBuffer(taskId);
27471
28444
  return c.json(toBufferResponse(buf));
27472
28445
  }).post(
27473
28446
  "/resize",
27474
- zv("json", z13.object({ workspaceId: z13.string(), taskId: z13.string(), cols: z13.number(), rows: z13.number() })),
28447
+ zv("json", z15.object({ workspaceId: z15.string(), taskId: z15.string(), cols: z15.number(), rows: z15.number() })),
27475
28448
  (c) => {
27476
28449
  const ctx = c.var.ctx;
27477
28450
  const { workspaceId, taskId, cols, rows } = c.req.valid("json");
27478
28451
  ctx.getScheduler(workspaceId)?.resizeTerminal(taskId, cols, rows);
27479
28452
  return c.json({ ok: true });
27480
28453
  }
27481
- ).post("/input", zv("json", z13.object({ workspaceId: z13.string(), taskId: z13.string(), data: z13.string() })), (c) => {
28454
+ ).post("/input", zv("json", z15.object({ workspaceId: z15.string(), taskId: z15.string(), data: z15.string() })), (c) => {
27482
28455
  const ctx = c.var.ctx;
27483
28456
  const { workspaceId, taskId, data } = c.req.valid("json");
27484
28457
  ctx.getScheduler(workspaceId)?.writeToTerminal(taskId, data);
@@ -27486,7 +28459,7 @@ var terminalController = new Hono2().get("/buffer", zv("query", z13.object({ wor
27486
28459
  });
27487
28460
 
27488
28461
  // src/api/routes/tunnel.ts
27489
- import { z as z14 } from "zod";
28462
+ import { z as z16 } from "zod";
27490
28463
 
27491
28464
  // src/api/services/tunnel-service.ts
27492
28465
  init_runtime_config();
@@ -27495,12 +28468,12 @@ init_runtime_config();
27495
28468
  import { execFile as execFile9, spawn as spawn7 } from "node:child_process";
27496
28469
  import { mkdir as mkdir4, writeFile as writeFile3, readFile as readFile3, access, unlink as unlink4 } from "node:fs/promises";
27497
28470
  import { homedir as homedir5 } from "node:os";
27498
- import { join as join20 } from "node:path";
28471
+ import { join as join21 } from "node:path";
27499
28472
  import { promisify as promisify9 } from "node:util";
27500
28473
  init_runtime_config();
27501
28474
  init_logger();
27502
28475
  var execFileAsync7 = promisify9(execFile9);
27503
- var CLOUDFLARED_DIR = join20(homedir5(), ".cloudflared");
28476
+ var CLOUDFLARED_DIR = join21(homedir5(), ".cloudflared");
27504
28477
  async function checkCloudflaredInstalled() {
27505
28478
  try {
27506
28479
  const { stdout } = await execFileAsync7("cloudflared", ["--version"]);
@@ -27512,7 +28485,7 @@ async function checkCloudflaredInstalled() {
27512
28485
  }
27513
28486
  async function checkCloudflaredAuth() {
27514
28487
  try {
27515
- await access(join20(CLOUDFLARED_DIR, "cert.pem"));
28488
+ await access(join21(CLOUDFLARED_DIR, "cert.pem"));
27516
28489
  return true;
27517
28490
  } catch {
27518
28491
  return false;
@@ -27523,7 +28496,7 @@ async function openCloudflaredLogin(force = false) {
27523
28496
  if (alreadyLoggedIn && !force) return { alreadyLoggedIn: true };
27524
28497
  if (force) {
27525
28498
  try {
27526
- await unlink4(join20(CLOUDFLARED_DIR, "cert.pem"));
28499
+ await unlink4(join21(CLOUDFLARED_DIR, "cert.pem"));
27527
28500
  } catch {
27528
28501
  }
27529
28502
  }
@@ -27592,7 +28565,7 @@ async function createTunnel(tunnelName) {
27592
28565
  }
27593
28566
  }
27594
28567
  async function writeTunnelConfig(tunnelId, tunnelName, domain) {
27595
- const credentialsFile = join20(CLOUDFLARED_DIR, `${tunnelId}.json`);
28568
+ const credentialsFile = join21(CLOUDFLARED_DIR, `${tunnelId}.json`);
27596
28569
  const config = [
27597
28570
  `tunnel: ${tunnelId}`,
27598
28571
  `credentials-file: ${credentialsFile}`,
@@ -27603,12 +28576,12 @@ async function writeTunnelConfig(tunnelId, tunnelName, domain) {
27603
28576
  ` - service: http_status:404`
27604
28577
  ].join("\n");
27605
28578
  await mkdir4(CLOUDFLARED_DIR, { recursive: true });
27606
- await writeFile3(join20(CLOUDFLARED_DIR, "config.yml"), config, "utf-8");
28579
+ await writeFile3(join21(CLOUDFLARED_DIR, "config.yml"), config, "utf-8");
27607
28580
  logger.info(`[tunnel-setup] Wrote ~/.cloudflared/config.yml for tunnel ${tunnelName}`);
27608
28581
  }
27609
28582
  async function readTunnelConfig() {
27610
28583
  try {
27611
- const raw2 = await readFile3(join20(CLOUDFLARED_DIR, "config.yml"), "utf-8");
28584
+ const raw2 = await readFile3(join21(CLOUDFLARED_DIR, "config.yml"), "utf-8");
27612
28585
  const tunnelMatch = raw2.match(/^tunnel:\s*(.+)$/m);
27613
28586
  const hostnameMatch = raw2.match(/hostname:\s*(.+)$/m);
27614
28587
  return {
@@ -27657,9 +28630,9 @@ var resetTunnel = async () => {
27657
28630
  await updateGlobalConfig({ tunnelId: void 0, tunnelDomain: void 0, autoStartTunnel: false });
27658
28631
  const { unlink: unlink5 } = await import("node:fs/promises");
27659
28632
  const { homedir: homedir6 } = await import("node:os");
27660
- const { join: join23 } = await import("node:path");
28633
+ const { join: join24 } = await import("node:path");
27661
28634
  try {
27662
- await unlink5(join23(homedir6(), ".cloudflared", "config.yml"));
28635
+ await unlink5(join24(homedir6(), ".cloudflared", "config.yml"));
27663
28636
  } catch {
27664
28637
  }
27665
28638
  };
@@ -27671,9 +28644,9 @@ var tunnelController = new Hono2().get("/checkCloudflared", async (c) => {
27671
28644
  return c.json(await getTunnelConfig());
27672
28645
  }).get("/tunnelStatus", async (c) => {
27673
28646
  return c.json(await getTunnelStatus());
27674
- }).post("/cloudflaredLogin", zv("json", z14.object({ force: z14.boolean().default(false) })), async (c) => {
28647
+ }).post("/cloudflaredLogin", zv("json", z16.object({ force: z16.boolean().default(false) })), async (c) => {
27675
28648
  return c.json(await cloudflaredLogin(c.req.valid("json").force));
27676
- }).post("/createTunnel", zv("json", z14.object({ domain: z14.string() })), async (c) => {
28649
+ }).post("/createTunnel", zv("json", z16.object({ domain: z16.string() })), async (c) => {
27677
28650
  return c.json(await createTunnel2(c.req.valid("json").domain));
27678
28651
  }).post("/startTunnel", async (c) => {
27679
28652
  return c.json(await startTunnel());
@@ -27686,11 +28659,11 @@ var tunnelController = new Hono2().get("/checkCloudflared", async (c) => {
27686
28659
 
27687
28660
  // src/api/routes/workflows.ts
27688
28661
  init_api_contract();
27689
- import { z as z15 } from "zod";
28662
+ import { z as z17 } from "zod";
27690
28663
 
27691
28664
  // src/api/services/workflows-service.ts
27692
28665
  init_workspace_state();
27693
- import { existsSync as existsSync13 } from "node:fs";
28666
+ import { existsSync as existsSync14 } from "node:fs";
27694
28667
  import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
27695
28668
  import { dirname as dirname8, isAbsolute as isAbsolute2, resolve as resolve4 } from "node:path";
27696
28669
  var resolvePromptPath = async (workspaceId, requestedPath) => {
@@ -27735,22 +28708,22 @@ var writePromptFile = async (workspaceId, path2, content) => {
27735
28708
  };
27736
28709
  var readPromptFile = async (workspaceId, path2) => {
27737
28710
  const targetPath = await resolvePromptPath(workspaceId, path2);
27738
- if (!existsSync13(targetPath)) return { content: "", exists: false };
28711
+ if (!existsSync14(targetPath)) return { content: "", exists: false };
27739
28712
  const content = await readFile4(targetPath, "utf-8");
27740
28713
  return { content, exists: true };
27741
28714
  };
27742
28715
 
27743
28716
  // src/api/routes/workflows.ts
27744
- var workflowsController = new Hono2().get("/", zv("query", z15.object({ workspaceId: z15.string() })), async (c) => {
28717
+ var workflowsController = new Hono2().get("/", zv("query", z17.object({ workspaceId: z17.string() })), async (c) => {
27745
28718
  const { workspaceId } = c.req.valid("query");
27746
28719
  return c.json(await listWorkflows(workspaceId));
27747
- }).post("/", zv("json", z15.object({ workspaceId: z15.string(), workflow: workflowSchema })), async (c) => {
28720
+ }).post("/", zv("json", z17.object({ workspaceId: z17.string(), workflow: workflowSchema })), async (c) => {
27748
28721
  const ctx = c.var.ctx;
27749
28722
  const { workspaceId, workflow } = c.req.valid("json");
27750
28723
  const result = await upsertWorkflow(workspaceId, workflow);
27751
28724
  ctx.stateHub.broadcastWorkspaceUpdate(workspaceId);
27752
28725
  return c.json(result);
27753
- }).delete("/:workflowId", zv("query", z15.object({ workspaceId: z15.string() })), async (c) => {
28726
+ }).delete("/:workflowId", zv("query", z17.object({ workspaceId: z17.string() })), async (c) => {
27754
28727
  const ctx = c.var.ctx;
27755
28728
  const { workspaceId } = c.req.valid("query");
27756
28729
  const workflowId = c.req.param("workflowId");
@@ -27759,23 +28732,23 @@ var workflowsController = new Hono2().get("/", zv("query", z15.object({ workspac
27759
28732
  return c.json(result);
27760
28733
  }).post(
27761
28734
  "/prompt-file",
27762
- zv("json", z15.object({ workspaceId: z15.string(), path: z15.string().min(1), content: z15.string() })),
28735
+ zv("json", z17.object({ workspaceId: z17.string(), path: z17.string().min(1), content: z17.string() })),
27763
28736
  async (c) => {
27764
28737
  const { workspaceId, path: path2, content } = c.req.valid("json");
27765
28738
  return c.json(await writePromptFile(workspaceId, path2, content));
27766
28739
  }
27767
- ).get("/prompt-file", zv("query", z15.object({ workspaceId: z15.string(), path: z15.string().min(1) })), async (c) => {
28740
+ ).get("/prompt-file", zv("query", z17.object({ workspaceId: z17.string(), path: z17.string().min(1) })), async (c) => {
27768
28741
  const { workspaceId, path: path2 } = c.req.valid("query");
27769
28742
  return c.json(await readPromptFile(workspaceId, path2));
27770
28743
  });
27771
28744
 
27772
28745
  // src/api/routes/workspace.ts
27773
28746
  init_api_contract();
27774
- import { z as z16 } from "zod";
28747
+ import { z as z18 } from "zod";
27775
28748
 
27776
28749
  // src/api/services/workspace-service.ts
27777
28750
  init_workspace_state();
27778
- import { spawnSync as spawnSync10 } from "node:child_process";
28751
+ import { spawnSync as spawnSync11 } from "node:child_process";
27779
28752
  var loadStateForWorkspace = async (workspaceId) => {
27780
28753
  const workspaces = await listWorkspaces();
27781
28754
  const ws = workspaces.find((w2) => w2.workspaceId === workspaceId);
@@ -27785,12 +28758,12 @@ var loadStateForWorkspace = async (workspaceId) => {
27785
28758
  var loadStateForContext = async (workspaceId, repoPath) => loadWorkspaceState(workspaceId, repoPath);
27786
28759
  var saveState = async (workspaceId, request2) => saveWorkspaceState(workspaceId, request2);
27787
28760
  var listRootFiles = (repoPath) => {
27788
- const ignored = spawnSync10(
28761
+ const ignored = spawnSync11(
27789
28762
  "git",
27790
28763
  ["ls-files", "--others", "--ignored", "--exclude-standard", "--directory", "--no-empty-directory"],
27791
28764
  { cwd: repoPath, encoding: "utf-8" }
27792
28765
  );
27793
- const untracked = spawnSync10("git", ["ls-files", "--others", "--exclude-standard"], {
28766
+ const untracked = spawnSync11("git", ["ls-files", "--others", "--exclude-standard"], {
27794
28767
  cwd: repoPath,
27795
28768
  encoding: "utf-8"
27796
28769
  });
@@ -27799,7 +28772,7 @@ var listRootFiles = (repoPath) => {
27799
28772
  };
27800
28773
 
27801
28774
  // src/api/routes/workspace.ts
27802
- var workspaceController = new Hono2().get("/state", zv("query", z16.object({ workspaceId: z16.string().optional() })), async (c) => {
28775
+ var workspaceController = new Hono2().get("/state", zv("query", z18.object({ workspaceId: z18.string().optional() })), async (c) => {
27803
28776
  const ctx = c.var.ctx;
27804
28777
  const { workspaceId } = c.req.valid("query");
27805
28778
  if (workspaceId) {
@@ -27807,10 +28780,10 @@ var workspaceController = new Hono2().get("/state", zv("query", z16.object({ wor
27807
28780
  }
27808
28781
  if (!ctx.currentWorkspaceId || !ctx.currentRepoPath) throw BadRequestError("No workspace context");
27809
28782
  return c.json(await loadStateForContext(ctx.currentWorkspaceId, ctx.currentRepoPath));
27810
- }).post("/save", zv("json", runtimeWorkspaceStateSaveRequestSchema.extend({ workspaceId: z16.string() })), async (c) => {
28783
+ }).post("/save", zv("json", runtimeWorkspaceStateSaveRequestSchema.extend({ workspaceId: z18.string() })), async (c) => {
27811
28784
  const { workspaceId, board, revision } = c.req.valid("json");
27812
28785
  return c.json(await saveState(workspaceId, { board, revision }));
27813
- }).get("/root-files", zv("query", z16.object({ workspaceId: z16.string() })), async (c) => {
28786
+ }).get("/root-files", zv("query", z18.object({ workspaceId: z18.string() })), async (c) => {
27814
28787
  const ctx = c.var.ctx;
27815
28788
  const { workspaceId } = c.req.valid("query");
27816
28789
  const ws = await ctx.ensureWorkspace(workspaceId);
@@ -27822,7 +28795,7 @@ function createApiApp(ctx) {
27822
28795
  const app = new Hono2().basePath("/api").use("*", async (c, next) => {
27823
28796
  c.set("ctx", ctx);
27824
28797
  await next();
27825
- }).get("/health", (c) => c.json({ ok: true })).route("/auth", authController).route("/agent", agentController).route("/agents", agentsController).route("/cards", cardsController).route("/config", configController).route("/fs", fsController).route("/memory", memoryController).route("/project-config", projectConfigController).route("/projects", projectsController).route("/recurring-agents", recurringAgentsController).route("/run", runController).route("/slack", slackController).route("/terminal", terminalController).route("/tunnel", tunnelController).route("/workflows", workflowsController).route("/workspace", workspaceController);
28798
+ }).get("/health", (c) => c.json({ ok: true })).route("/auth", authController).route("/agent", agentController).route("/agents", agentsController).route("/cards", cardsController).route("/companion-saved-canvases", companionSavedCanvasesController).route("/companion-sessions", companionSessionsController).route("/config", configController).route("/fs", fsController).route("/memory", memoryController).route("/project-config", projectConfigController).route("/projects", projectsController).route("/recurring-agents", recurringAgentsController).route("/run", runController).route("/slack", slackController).route("/terminal", terminalController).route("/tunnel", tunnelController).route("/workflows", workflowsController).route("/workspace", workspaceController);
27826
28799
  app.onError(errorHandler2);
27827
28800
  return app;
27828
28801
  }
@@ -27932,6 +28905,9 @@ var RuntimeStateHub = class {
27932
28905
  broadcastRunSessionChange(workspaceId, cardId, status, errorMessage) {
27933
28906
  this.broadcastToWorkspace(workspaceId, { type: "run_session_changed", cardId, status, errorMessage });
27934
28907
  }
28908
+ broadcastCompanionCanvasUpdate(workspaceId, sessionId, canvas) {
28909
+ this.broadcastToWorkspace(workspaceId, { type: "companion_canvas_updated", sessionId, canvas });
28910
+ }
27935
28911
  broadcastToWorkspace(workspaceId, event) {
27936
28912
  const clientIds = this.workspaceClients.get(workspaceId);
27937
28913
  if (!clientIds) return;
@@ -27953,8 +28929,8 @@ var RuntimeStateHub = class {
27953
28929
  // src/core/update-check.ts
27954
28930
  init_paths();
27955
28931
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
27956
- import { join as join21 } from "node:path";
27957
- var CACHE_FILE = join21(WHIPPED_HOME_DIR, "update-check.json");
28932
+ import { join as join22 } from "node:path";
28933
+ var CACHE_FILE = join22(WHIPPED_HOME_DIR, "update-check.json");
27958
28934
  var REGISTRY_URL = "https://registry.npmjs.org/whipped/latest";
27959
28935
  var CHECK_INTERVAL_MS = 12 * 60 * 60 * 1e3;
27960
28936
  function readCache() {
@@ -28013,6 +28989,10 @@ async function cleanupStaleTasks(workspaceId, hub) {
28013
28989
  if (staleRecurring > 0) {
28014
28990
  logger.info(`[server] Cleared ${staleRecurring} stale recurring-agent run(s) for ${workspaceId}`);
28015
28991
  }
28992
+ const staleCompanions = resetStaleCompanionSessions(workspaceId);
28993
+ if (staleCompanions > 0) {
28994
+ logger.info(`[server] Reset ${staleCompanions} stale companion session(s) for ${workspaceId}`);
28995
+ }
28016
28996
  for (const card of Object.values(board.cards)) {
28017
28997
  if (card.terminalSessions?.some((s2) => s2.endedAt === void 0)) {
28018
28998
  await closeAllOpenTerminalSessions(workspaceId, card.id, now);
@@ -28241,9 +29221,9 @@ async function createRuntimeServer(options) {
28241
29221
  };
28242
29222
  }
28243
29223
  const apiApp = createApiApp(createContext());
28244
- const webUiDistPath = join22(__dirname2, "web-ui");
28245
- const webUiIndexPath = join22(webUiDistPath, "index.html");
28246
- const hasWebUi = existsSync14(webUiIndexPath);
29224
+ const webUiDistPath = join23(__dirname2, "web-ui");
29225
+ const webUiIndexPath = join23(webUiDistPath, "index.html");
29226
+ const hasWebUi = existsSync15(webUiIndexPath);
28247
29227
  const httpServer = createServer(async (req, res) => {
28248
29228
  const url = new URL(req.url ?? "/", `http://${host}`);
28249
29229
  if (url.pathname === "/api/slack/oauth-callback") {
@@ -28476,7 +29456,7 @@ async function createRuntimeServer(options) {
28476
29456
  res.end("Bad request");
28477
29457
  return;
28478
29458
  }
28479
- const filePath = join22(ATTACHMENTS_DIR, cardId, filename);
29459
+ const filePath = join23(ATTACHMENTS_DIR, cardId, filename);
28480
29460
  const { readFile: readFile5 } = await import("node:fs/promises");
28481
29461
  try {
28482
29462
  const data = await readFile5(filePath);
@@ -28551,8 +29531,8 @@ async function createRuntimeServer(options) {
28551
29531
  return;
28552
29532
  }
28553
29533
  if (hasWebUi) {
28554
- const filePath = url.pathname === "/" || !url.pathname.includes(".") ? webUiIndexPath : join22(webUiDistPath, url.pathname);
28555
- if (existsSync14(filePath)) {
29534
+ const filePath = url.pathname === "/" || !url.pathname.includes(".") ? webUiIndexPath : join23(webUiDistPath, url.pathname);
29535
+ if (existsSync15(filePath)) {
28556
29536
  const content = readFileSync8(filePath);
28557
29537
  res.writeHead(200, { "Content-Type": getContentType(filePath) });
28558
29538
  res.end(content);
@@ -28632,15 +29612,17 @@ async function createRuntimeServer(options) {
28632
29612
  if (activeBuffer !== null) {
28633
29613
  if (activeBuffer && ws.readyState === 1) ws.send(prepareBufferForReplay(activeBuffer));
28634
29614
  } else {
28635
- const hubSnapshot = stateHub.getTerminalBuffer(workspaceId, taskId);
28636
- if (hubSnapshot) {
28637
- if (ws.readyState === 1) ws.send(prepareBufferForReplay(hubSnapshot));
28638
- } else {
28639
- loadTerminalBuffer(workspaceId, taskId).then((diskSnapshot) => {
28640
- if (diskSnapshot && ws.readyState === 1) ws.send(prepareBufferForReplay(diskSnapshot));
28641
- }).catch(() => {
28642
- });
28643
- }
29615
+ loadTerminalBuffer(workspaceId, taskId).then((diskSnapshot) => {
29616
+ if (diskSnapshot) {
29617
+ if (ws.readyState === 1) ws.send(prepareBufferForReplay(diskSnapshot));
29618
+ return;
29619
+ }
29620
+ const hubSnapshot = stateHub.getTerminalBuffer(workspaceId, taskId);
29621
+ if (hubSnapshot && ws.readyState === 1) ws.send(prepareBufferForReplay(hubSnapshot));
29622
+ }).catch(() => {
29623
+ const hubSnapshot = stateHub.getTerminalBuffer(workspaceId, taskId);
29624
+ if (hubSnapshot && ws.readyState === 1) ws.send(prepareBufferForReplay(hubSnapshot));
29625
+ });
28644
29626
  }
28645
29627
  ws.on("message", (raw2) => {
28646
29628
  const text = raw2.toString();
@@ -28720,6 +29702,9 @@ async function createRuntimeServer(options) {
28720
29702
  await writeClaudeTaskHookSettings(port).catch((err) => {
28721
29703
  logger.warn("[server] Failed to write claude hook settings:", err);
28722
29704
  });
29705
+ await writeClaudeCompanionSettings().catch((err) => {
29706
+ logger.warn("[server] Failed to write claude companion settings:", err);
29707
+ });
28723
29708
  if (_globalConfig.autoStartTunnel) tunnelManager.start();
28724
29709
  scheduleUpdateChecks(VERSION10, (latestVersion) => {
28725
29710
  stateHub.broadcastUpdateAvailable(latestVersion);
@@ -28770,7 +29755,7 @@ process.on("uncaughtException", (err) => {
28770
29755
  if (err.code === "EPIPE" || err.code === "ECONNRESET") return;
28771
29756
  throw err;
28772
29757
  });
28773
- var VERSION9 = true ? "0.8.1" : "0.0.0-dev";
29758
+ var VERSION9 = true ? "0.9.1" : "0.0.0-dev";
28774
29759
  async function isPortAvailable(port, host) {
28775
29760
  return new Promise((resolve5) => {
28776
29761
  const probe = createServer2();