whipped 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/cli.js +1294 -259
  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-RwOyl_Kz.js +119 -0
  10. package/dist/web-ui/assets/arc-DmDBHE0H.js +131 -0
  11. package/dist/web-ui/assets/architectureDiagram-ZJ3FMSHR-RTwadm6J.js +8821 -0
  12. package/dist/web-ui/assets/blockDiagram-677ZJIJ3-Dvv5uMUE.js +3801 -0
  13. package/dist/web-ui/assets/c4Diagram-LMCZKHZV-bvr9R9cD.js +2479 -0
  14. package/dist/web-ui/assets/channel-BGhlETgZ.js +7 -0
  15. package/dist/web-ui/assets/chunk-2Q5K7J3B-BRq-Qbau.js +17 -0
  16. package/dist/web-ui/assets/chunk-32BRIVSS-Dy1BUZGx.js +116 -0
  17. package/dist/web-ui/assets/chunk-5VM5RSS4-DCUiIwIc.js +19 -0
  18. package/dist/web-ui/assets/chunk-EX3LRPZG-Cg_Vtzwz.js +1996 -0
  19. package/dist/web-ui/assets/chunk-JWPE2WC7-BW4n_ZhH.js +17 -0
  20. package/dist/web-ui/assets/chunk-MOJQB5TN-BykRa615.js +855 -0
  21. package/dist/web-ui/assets/chunk-RYQCIY6F-D4F7oV1d.js +476 -0
  22. package/dist/web-ui/assets/chunk-V7JOEXUC-DD4mm30h.js +2022 -0
  23. package/dist/web-ui/assets/chunk-VR4S4FIN-Fgvcluvk.js +25 -0
  24. package/dist/web-ui/assets/chunk-XXDRQBXY-C4FVmO5r.js +13 -0
  25. package/dist/web-ui/assets/classDiagram-OUVF2IWQ-DD4KIYF1.js +24 -0
  26. package/dist/web-ui/assets/classDiagram-v2-EOCWNBFH-DD4KIYF1.js +24 -0
  27. package/dist/web-ui/assets/cose-bilkent-JH36ORCC-ekFwvYt9.js +4943 -0
  28. package/dist/web-ui/assets/cynefin-VYW2F7L2-DTNV7gvQ.js +31527 -0
  29. package/dist/web-ui/assets/cynefinDiagram-TSTJHNR4-koYialeC.js +454 -0
  30. package/dist/web-ui/assets/cytoscape.esm-CaQ7Fomf.js +30346 -0
  31. package/dist/web-ui/assets/dagre-VKFMJZFB-Do43FV3o.js +526 -0
  32. package/dist/web-ui/assets/defaultLocale-B2RvLBDe.js +206 -0
  33. package/dist/web-ui/assets/diagram-FQU43EPY-D0STdny6.js +636 -0
  34. package/dist/web-ui/assets/diagram-G47NLZAW-D9g6BdZT.js +858 -0
  35. package/dist/web-ui/assets/diagram-NH7WQ7WH-zLW6CAmi.js +212 -0
  36. package/dist/web-ui/assets/diagram-OA4YK3LP-CA5tvsYw.js +492 -0
  37. package/dist/web-ui/assets/diagram-WEI45ONY-CLmYUHR0.js +309 -0
  38. package/dist/web-ui/assets/ebnfDiagram-CCIWWBDH-KkHubBI6.js +139 -0
  39. package/dist/web-ui/assets/erDiagram-Q63AITRT-BJna2u1n.js +1238 -0
  40. package/dist/web-ui/assets/flowDiagram-23GEKE2U-DVJKalah.js +2353 -0
  41. package/dist/web-ui/assets/ganttDiagram-NO4QXBWP-D9HwNV4u.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-B7wnoO0J.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-DZ7I8r_C.js} +41392 -39594
  57. package/dist/web-ui/assets/infoDiagram-FWYZ7A6U-BY6XoiF8.js +32 -0
  58. package/dist/web-ui/assets/init-ZxktEp_H.js +16 -0
  59. package/dist/web-ui/assets/ishikawaDiagram-FXEZZL3T-BaZVnO8j.js +967 -0
  60. package/dist/web-ui/assets/journeyDiagram-5HDEW3XC-CUA6DUAQ.js +1256 -0
  61. package/dist/web-ui/assets/kanban-definition-HUTT4EX6-5W5tiWrd.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-CfvGIyDE.js +340 -0
  65. package/dist/web-ui/assets/map-BEO0Bu8q.js +298 -0
  66. package/dist/web-ui/assets/mermaid.core-BRk3IzY2.js +26639 -0
  67. package/dist/web-ui/assets/mindmap-definition-LN4V7U3C-2GmLg6ou.js +1183 -0
  68. package/dist/web-ui/assets/ordinal-DSZU4PqD.js +76 -0
  69. package/dist/web-ui/assets/pegDiagram-2B236MQR-gTEdrkJg.js +127 -0
  70. package/dist/web-ui/assets/pieDiagram-ENE6RG2P-CYXjIhqC.js +318 -0
  71. package/dist/web-ui/assets/quadrantDiagram-ABIIQ3AL-BStRZxwf.js +1341 -0
  72. package/dist/web-ui/assets/railroadDiagram-RFXS5EU6-btveDRG2.js +93 -0
  73. package/dist/web-ui/assets/requirementDiagram-TGXJPOKE-Cy_155rE.js +1205 -0
  74. package/dist/web-ui/assets/sankeyDiagram-HTMAVEWB-Chtvw3_G.js +1264 -0
  75. package/dist/web-ui/assets/sequenceDiagram-DBY2YBRQ-DDuMVEX1.js +4523 -0
  76. package/dist/web-ui/assets/sizeCapture-X5ZJPWSS-Bylf0o6J.js +64 -0
  77. package/dist/web-ui/assets/stateDiagram-2N3HPSRC-DIzLeR5G.js +453 -0
  78. package/dist/web-ui/assets/stateDiagram-v2-6OUMAXLB-zG_WjT1-.js +23 -0
  79. package/dist/web-ui/assets/swimlanes-5IMT3BWC-TKaCmVta.js +8575 -0
  80. package/dist/web-ui/assets/swimlanesDiagram-G3AALYLV-C5eB3qqS.js +21 -0
  81. package/dist/web-ui/assets/timeline-definition-FHXFAJF6-CC5Ujpcu.js +1606 -0
  82. package/dist/web-ui/assets/vennDiagram-L72KCM5P-DUSVXSYT.js +2523 -0
  83. package/dist/web-ui/assets/wardleyDiagram-EHGQE667-CoP9xn89.js +978 -0
  84. package/dist/web-ui/assets/xychartDiagram-FW5EYKEG-B9FqP_kk.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 readFileSync9 } 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,278 @@ 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 INLINE_DIFF_LIMIT = 8e3;
20881
+ function formatDiffBlock(fullDiff, baseRef, header = "Git diff") {
20882
+ if (fullDiff.length <= INLINE_DIFF_LIMIT) return `${header}:
20883
+ ${fullDiff}`;
20884
+ return `Large changeset (${fullDiff.length.toLocaleString()} chars). Use \`git diff ${baseRef}...HEAD\` and read individual files to explore.`;
20885
+ }
20886
+
20887
+ // src/daemon/review-pipeline.ts
20888
+ import { existsSync as existsSync9 } from "node:fs";
20889
+ import { readdir, readFile as readFile2, stat, unlink } from "node:fs/promises";
20890
+ import { join as join17 } from "node:path";
20518
20891
  init_runtime_config();
20519
20892
  init_api_contract();
20520
20893
  init_logger();
@@ -20799,7 +21172,7 @@ function getSlotTriggerWord(type) {
20799
21172
  }
20800
21173
  var SCREENSHOT_EXTENSIONS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "webp"]);
20801
21174
  async function attachBrowserArtifacts(workspaceId, card, result, since) {
20802
- const dir = join16(ATTACHMENTS_DIR, card.id);
21175
+ const dir = join17(ATTACHMENTS_DIR, card.id);
20803
21176
  let entries;
20804
21177
  try {
20805
21178
  entries = await readdir(dir);
@@ -20812,7 +21185,7 @@ async function attachBrowserArtifacts(workspaceId, card, result, since) {
20812
21185
  for (const name of entries) {
20813
21186
  const ext = name.split(".").pop()?.toLowerCase() ?? "";
20814
21187
  if (!SCREENSHOT_EXTENSIONS.has(ext)) continue;
20815
- const filePath = join16(dir, name);
21188
+ const filePath = join17(dir, name);
20816
21189
  try {
20817
21190
  const info2 = await stat(filePath);
20818
21191
  if (!info2.isFile() || info2.mtimeMs < since) continue;
@@ -21077,61 +21450,6 @@ function runAgentOnce(agentId, prompt, cwd, workspaceId, streamId, stateHub, reg
21077
21450
  unregisterProcess = registerLiveProcess(streamId, proc);
21078
21451
  });
21079
21452
  }
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
- }
21089
- }
21090
- function getGitStat(worktreePath, baseRef) {
21091
- 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
21453
  function attachmentLines(attachments) {
21136
21454
  return attachments.map((a, i) => `- [Attachment #${i + 1}] ${a.name}: ${a.path}`).join("\n");
21137
21455
  }
@@ -21257,12 +21575,6 @@ ${sections.join("\n\n---\n\n")}`,
21257
21575
  files: []
21258
21576
  };
21259
21577
  }
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
21578
  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
21579
  function renderReviewDiff(stat3, fullDiff, baseRef, scope) {
21268
21580
  if (!scope.useIncremental) {
@@ -21764,16 +22076,75 @@ function tryParseAgentJson(output) {
21764
22076
  }
21765
22077
  }
21766
22078
 
21767
- // src/daemon/scheduler.ts
21768
- var FAST_EXIT_THRESHOLD_MS = 8e3;
21769
- var MAX_RECENT_BUFFERS = 100;
21770
- var TaskScheduler = class {
21771
- constructor(options) {
21772
- this.options = options;
22079
+ // src/daemon/companion-agent.ts
22080
+ function buildCompanionAgentSystemPrompt(workspaceId, repoPath, worktreePath, baseRef, secrets, systemPrompt, gitInstructions, seedPrompt, resumedCanvas) {
22081
+ const effectiveGitInstructions = gitInstructions?.trim() || DEFAULT_GIT_INSTRUCTIONS;
22082
+ const fullDiff = getGitFullDiff(worktreePath, baseRef);
22083
+ const worktreeSection = fullDiff ? `## Current worktree state (vs ${baseRef})
22084
+ ${getGitStat(worktreePath, baseRef)}
22085
+
22086
+ ## Diff (vs ${baseRef})
22087
+ ${formatDiffBlock(fullDiff, baseRef, "Git diff")}` : `## Worktree state
22088
+
22089
+ The worktree is clean and branched from \`${baseRef}\` \u2014 there is no diff yet. Skip \`git diff\` and start working.`;
22090
+ const parts = [
22091
+ `You are the Companion agent for the project at \`${repoPath}\`.
22092
+
22093
+ 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}\`.
22094
+
22095
+ 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.`,
22096
+ worktreeSection
22097
+ ];
22098
+ parts.push(`## Sharing a canvas with the developer
22099
+
22100
+ 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.
22101
+
22102
+ ${buildCanvasModeGuidance()}`);
22103
+ if (resumedCanvas) {
22104
+ parts.push(`## Resuming a saved canvas
22105
+
22106
+ 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.
22107
+
22108
+ ${serializeCanvasBlocksForPrompt(resumedCanvas.blocks)}`);
22109
+ }
22110
+ if (seedPrompt?.trim()) parts.push(`## Project-specific instructions
22111
+
22112
+ ${seedPrompt.trim()}`);
22113
+ parts.push(`## Memory
22114
+
22115
+ 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.
22116
+
22117
+ 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.
22118
+
22119
+ 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.
22120
+
22121
+ Scope a memory \`project\` for facts specific to this repo, or \`global\` for things that apply across all the user's projects (style/preferences).`);
22122
+ const secretsSection = buildSecretsSection(secrets);
22123
+ if (secretsSection) parts.push(secretsSection);
22124
+ if (systemPrompt?.trim()) parts.push(`## Project context
22125
+
22126
+ ${systemPrompt.trim()}`);
22127
+ parts.push(`## Git conventions
22128
+
22129
+ ${effectiveGitInstructions}`);
22130
+ const memContext = buildMemoryContext(workspaceId);
22131
+ const text = parts.join("\n\n");
22132
+ return memContext ? `${memContext}
22133
+
22134
+ ${text}` : text;
22135
+ }
22136
+
22137
+ // src/daemon/scheduler.ts
22138
+ var FAST_EXIT_THRESHOLD_MS = 8e3;
22139
+ var MAX_RECENT_BUFFERS = 100;
22140
+ var TaskScheduler = class {
22141
+ constructor(options) {
22142
+ this.options = options;
21773
22143
  }
21774
22144
  options;
21775
22145
  running = /* @__PURE__ */ new Map();
21776
22146
  assistantSessions = /* @__PURE__ */ new Map();
22147
+ companionSessions = /* @__PURE__ */ new Map();
21777
22148
  // Keep the last output buffer around after a task exits so the terminal
21778
22149
  // can still restore when the user opens it for a completed/awaiting-review task.
21779
22150
  recentBuffers = /* @__PURE__ */ new Map();
@@ -21829,7 +22200,7 @@ var TaskScheduler = class {
21829
22200
  get assistantAgentTaskId() {
21830
22201
  return `${ASSISTANT_AGENT_PREFIX}${this.options.workspaceId}`;
21831
22202
  }
21832
- async startAssistantAgent() {
22203
+ async startAssistantAgent(override, savedCanvasId) {
21833
22204
  const { workspaceId, repoPath, serverUrl, stateHub, defaultAgent } = this.options;
21834
22205
  const taskId = this.assistantAgentTaskId;
21835
22206
  const existing = this.assistantSessions.get(taskId);
@@ -21841,11 +22212,18 @@ var TaskScheduler = class {
21841
22212
  stateHub.clearTerminalBuffer(workspaceId, taskId);
21842
22213
  const prompt = "";
21843
22214
  const projectConfig = await loadProjectConfig(workspaceId);
21844
- const assistantModel = projectConfig.assistantModel;
22215
+ const assistantModel = override ?? projectConfig.assistantModel;
21845
22216
  const agentId = assistantModel?.agentId ?? defaultAgent;
21846
22217
  const secrets = projectConfig.secrets ?? [];
21847
22218
  const secretsEnv = buildSecretsEnv(secrets);
21848
- const assistantSystemPrompt = buildAssistantAgentSystemPrompt(repoPath, secrets, projectConfig.systemPrompt);
22219
+ const savedCanvas = savedCanvasId ? getCompanionSavedCanvas(savedCanvasId) : null;
22220
+ if (savedCanvas) createCompanionCanvas(taskId, workspaceId, savedCanvas.blocks);
22221
+ const assistantSystemPrompt = buildAssistantAgentSystemPrompt(
22222
+ repoPath,
22223
+ secrets,
22224
+ projectConfig.systemPrompt,
22225
+ savedCanvas ? { title: savedCanvas.title, blocks: savedCanvas.blocks } : void 0
22226
+ );
21849
22227
  const memContext = buildMemoryContext(workspaceId);
21850
22228
  const appendSystemPrompt = memContext ? `${memContext}
21851
22229
 
@@ -21896,6 +22274,9 @@ ${assistantSystemPrompt}` : assistantSystemPrompt;
21896
22274
  ...agentId === "cursor" ? { [CURSOR_CONFIG_DIR_ENV]: getCursorConfigDir(taskId) } : {}
21897
22275
  },
21898
22276
  mcpConfigPath: agentId === "claude" ? CLAUDE_ASSISTANT_MCP_CONFIG_PATH : void 0,
22277
+ // Denies Claude's own native plan-mode tools — see writeClaudeCompanionSettings
22278
+ // — so "plan" always means whipped_show_canvas, same as the companion agent.
22279
+ hookSettingsPath: agentId === "claude" ? CLAUDE_COMPANION_SETTINGS_PATH : void 0,
21899
22280
  mcpServer: agentId === "codex" ? buildWhippedMcpServerSpec(
21900
22281
  getMcpServerPath(),
21901
22282
  serverUrl,
@@ -21932,6 +22313,234 @@ ${assistantSystemPrompt}` : assistantSystemPrompt;
21932
22313
  isAssistantAgentRunning() {
21933
22314
  return this.assistantSessions.has(this.assistantAgentTaskId);
21934
22315
  }
22316
+ // Creates the worktree (or resolves the main-repo checkout) synchronously and
22317
+ // returns quickly — install + agent spawn happen in the background via
22318
+ // launchCompanionAgent so callers (the create-session API request) aren't
22319
+ // blocked on a potentially long install command.
22320
+ async startCompanionAgent(session) {
22321
+ const { workspaceId, repoPath, stateHub } = this.options;
22322
+ const taskId = session.id;
22323
+ const existing = this.companionSessions.get(taskId);
22324
+ if (existing) existing.process.kill();
22325
+ this.recentBuffers.delete(taskId);
22326
+ stateHub.clearTerminalBuffer(workspaceId, taskId);
22327
+ if (session.useWorktree) {
22328
+ const worktree = createWorktree(taskId, repoPath, session.baseRef, session.branchName ?? void 0);
22329
+ setCompanionSessionWorktreePath(taskId, worktree.path);
22330
+ setCompanionSessionStatus(taskId, worktree.isNew ? "installing" : "running");
22331
+ void this.launchCompanionAgent(session, worktree.path, worktree.isNew);
22332
+ } else {
22333
+ setCompanionSessionWorktreePath(taskId, repoPath);
22334
+ setCompanionSessionStatus(taskId, "running");
22335
+ void this.launchCompanionAgent(session, repoPath, false);
22336
+ }
22337
+ }
22338
+ async launchCompanionAgent(session, cwd, isNewWorktree) {
22339
+ const { workspaceId, repoPath, serverUrl, stateHub } = this.options;
22340
+ const taskId = session.id;
22341
+ const agentId = session.agentId;
22342
+ const projectConfig = await loadProjectConfig(workspaceId);
22343
+ const secrets = projectConfig.secrets ?? [];
22344
+ const secretsEnv = buildSecretsEnv(secrets);
22345
+ if (isNewWorktree && projectConfig.worktreeSetup) {
22346
+ await this.runCompanionInstall(taskId, workspaceId, repoPath, cwd, projectConfig.worktreeSetup);
22347
+ if (this.isShuttingDown) return;
22348
+ if (this.manuallyStoppedInstalls.delete(taskId)) {
22349
+ await removeWorktreeAsync(taskId, repoPath, session.branchName ?? void 0);
22350
+ setCompanionSessionWorktreePath(taskId, null);
22351
+ setCompanionSessionStatus(taskId, "stopped");
22352
+ return;
22353
+ }
22354
+ }
22355
+ setCompanionSessionStatus(taskId, "running");
22356
+ const resumedCanvas = session.savedCanvasId ? getCompanionSavedCanvas(session.savedCanvasId) : null;
22357
+ const appendSystemPrompt = buildCompanionAgentSystemPrompt(
22358
+ workspaceId,
22359
+ repoPath,
22360
+ cwd,
22361
+ session.baseRef,
22362
+ secrets,
22363
+ projectConfig.systemPrompt,
22364
+ projectConfig.gitInstructions,
22365
+ session.seedPrompt,
22366
+ resumedCanvas ? { title: resumedCanvas.title, blocks: resumedCanvas.blocks } : void 0
22367
+ );
22368
+ const mcpConfigPath = !isPluginConfigAgent(agentId) && agentId !== "cursor" ? getMcpConfigPath(taskId) : void 0;
22369
+ if (agentId === "claude") {
22370
+ await writeClaudeMcpConfig(
22371
+ getMcpServerPath(),
22372
+ serverUrl,
22373
+ workspaceId,
22374
+ agentId,
22375
+ mcpConfigPath,
22376
+ void 0,
22377
+ buildMcpRoleArgs("companion", void 0, taskId)
22378
+ ).catch((err) => {
22379
+ logger.warn({ err }, "[scheduler] Failed to write companion agent MCP config");
22380
+ });
22381
+ } else if (isPluginConfigAgent(agentId)) {
22382
+ const mcpSpec = buildWhippedMcpServerSpec(
22383
+ getMcpServerPath(),
22384
+ serverUrl,
22385
+ workspaceId,
22386
+ agentId,
22387
+ buildMcpRoleArgs("companion", void 0, taskId)
22388
+ );
22389
+ await writePluginAgentFiles(agentId, taskId, getServerPort(serverUrl), mcpSpec, { appendSystemPrompt }).catch(
22390
+ (err) => {
22391
+ logger.warn({ err }, `[scheduler] Failed to write ${agentId} companion agent files`);
22392
+ }
22393
+ );
22394
+ } else if (agentId === "cursor") {
22395
+ const mcpSpec = buildWhippedMcpServerSpec(
22396
+ getMcpServerPath(),
22397
+ serverUrl,
22398
+ workspaceId,
22399
+ agentId,
22400
+ buildMcpRoleArgs("companion", void 0, taskId)
22401
+ );
22402
+ await writeCursorConfigFiles(taskId, getServerPort(serverUrl), mcpSpec).catch((err) => {
22403
+ logger.warn({ err }, "[scheduler] Failed to write cursor companion agent config");
22404
+ });
22405
+ }
22406
+ let resolveExit;
22407
+ const exitPromise = new Promise((resolve5) => {
22408
+ resolveExit = resolve5;
22409
+ });
22410
+ const companionTask = {
22411
+ taskId,
22412
+ streamId: taskId,
22413
+ // companion session uses taskId as its stream (single persistent session)
22414
+ agentId,
22415
+ exitPromise,
22416
+ process: spawnAgent({
22417
+ agentId,
22418
+ prompt: "",
22419
+ cwd,
22420
+ env: {
22421
+ ...secretsEnv,
22422
+ ...buildTaskHookEnv(taskId, workspaceId),
22423
+ WHIPPED_SLOT: "companion",
22424
+ ...pluginAgentConfigDirEnv(agentId, taskId),
22425
+ ...agentId === "cursor" ? { [CURSOR_CONFIG_DIR_ENV]: getCursorConfigDir(taskId) } : {}
22426
+ },
22427
+ mcpConfigPath: agentId === "claude" ? mcpConfigPath : void 0,
22428
+ // Denies Claude's own native plan-mode tools for this session — see
22429
+ // writeClaudeCompanionSettings — so "plan" always means whipped_show_canvas.
22430
+ hookSettingsPath: agentId === "claude" ? CLAUDE_COMPANION_SETTINGS_PATH : void 0,
22431
+ mcpServer: agentId === "codex" ? buildWhippedMcpServerSpec(
22432
+ getMcpServerPath(),
22433
+ serverUrl,
22434
+ workspaceId,
22435
+ agentId,
22436
+ buildMcpRoleArgs("companion", void 0, taskId)
22437
+ ) : void 0,
22438
+ model: session.model ?? null,
22439
+ effort: session.effort ?? null,
22440
+ appendSystemPrompt: isPluginConfigAgent(agentId) ? void 0 : appendSystemPrompt,
22441
+ onOutput: (data) => {
22442
+ companionTask.outputBuffer += data;
22443
+ stateHub.broadcastTerminalOutput(workspaceId, taskId, data);
22444
+ },
22445
+ onExit: () => {
22446
+ this.setRecentBuffer(taskId, companionTask.outputBuffer);
22447
+ this.companionSessions.delete(taskId);
22448
+ setCompanionSessionStatus(taskId, "stopped");
22449
+ resolveExit();
22450
+ }
22451
+ }),
22452
+ startedAt: Date.now(),
22453
+ outputBuffer: ""
22454
+ };
22455
+ this.companionSessions.set(taskId, companionTask);
22456
+ }
22457
+ // Copies/links worktreeSetup's configured files then runs its install command,
22458
+ // mirroring the card dev-agent's worktree setup step (launchDevAgent above) but
22459
+ // streamed straight into the companion session's own terminal (taskId) instead
22460
+ // of a separate install stream row, since companion sessions have no such table.
22461
+ async runCompanionInstall(taskId, workspaceId, repoPath, worktreePath, worktreeSetup) {
22462
+ const { stateHub } = this.options;
22463
+ const { filesToCopy, installCommand } = worktreeSetup;
22464
+ for (const entry of filesToCopy) {
22465
+ const src = join18(repoPath, entry.path);
22466
+ if (!existsSync10(src)) continue;
22467
+ const dst = join18(worktreePath, entry.path);
22468
+ await mkdir3(dirname6(dst), { recursive: true });
22469
+ try {
22470
+ if (entry.symlink) {
22471
+ await shareIntoWorktree(src, dst);
22472
+ } else {
22473
+ await cp(src, dst, { recursive: true, dereference: true });
22474
+ }
22475
+ } catch (err) {
22476
+ logger.warn(
22477
+ { err },
22478
+ `[scheduler] companion worktree setup: failed to ${entry.symlink ? "link" : "copy"} ${entry.path}`
22479
+ );
22480
+ }
22481
+ }
22482
+ const installCmd = installCommand.trim();
22483
+ if (!installCmd) return;
22484
+ let buffer = "";
22485
+ const emit = (data) => {
22486
+ buffer += data;
22487
+ this.recentBuffers.set(taskId, buffer);
22488
+ stateHub.broadcastTerminalOutput(workspaceId, taskId, data);
22489
+ };
22490
+ emit(`\x1B[1;36m$ ${installCmd}\x1B[0m\r
22491
+ `);
22492
+ const exitCode = await new Promise((resolveExit) => {
22493
+ const [shell, shellArgs] = getShellInvocation(installCmd);
22494
+ const proc = nodePty2.spawn(shell, shellArgs, {
22495
+ name: "xterm-256color",
22496
+ cols: 220,
22497
+ rows: 50,
22498
+ cwd: worktreePath,
22499
+ env: { ...process.env, REPO_PATH: repoPath, TERM: "xterm-256color" }
22500
+ });
22501
+ this.runningInstalls.set(taskId, proc);
22502
+ proc.onData(emit);
22503
+ proc.onExit(({ exitCode: exitCode2 }) => resolveExit(exitCode2 ?? 0));
22504
+ });
22505
+ this.runningInstalls.delete(taskId);
22506
+ if (this.isShuttingDown) return;
22507
+ if (this.manuallyStoppedInstalls.has(taskId)) {
22508
+ emit("\r\n\x1B[1;33mInstall stopped\x1B[0m\r\n");
22509
+ return;
22510
+ }
22511
+ if (exitCode !== 0) {
22512
+ logger.error(`[scheduler] Install command failed (code ${exitCode}) for companion session ${taskId}`);
22513
+ emit(`\r
22514
+ \x1B[1;31mInstall command failed (code ${exitCode}) \u2014 proceeding anyway\x1B[0m\r
22515
+ `);
22516
+ } else {
22517
+ emit("\r\n\x1B[1;32mInstall complete\x1B[0m\r\n");
22518
+ }
22519
+ }
22520
+ // Kills the session's process (or its still-running install command) and waits
22521
+ // for it to actually exit (bounded by a timeout) before returning — callers
22522
+ // that are about to delete or merge the worktree on disk must await this
22523
+ // first, since attemptMerge/removeWorktreeAsync can otherwise race a still-live
22524
+ // process whose cwd is inside that worktree.
22525
+ async stopCompanionAgent(sessionId) {
22526
+ const installProc = this.runningInstalls.get(sessionId);
22527
+ if (installProc) {
22528
+ this.manuallyStoppedInstalls.add(sessionId);
22529
+ this.runningInstalls.delete(sessionId);
22530
+ killInstallProcess(installProc);
22531
+ }
22532
+ const session = this.companionSessions.get(sessionId);
22533
+ if (session) {
22534
+ const exitPromise = session.exitPromise ?? Promise.resolve();
22535
+ session.process.kill();
22536
+ await Promise.race([exitPromise, new Promise((resolve5) => setTimeout(resolve5, 5e3))]);
22537
+ }
22538
+ this.companionSessions.delete(sessionId);
22539
+ setCompanionSessionStatus(sessionId, "stopped");
22540
+ }
22541
+ isCompanionAgentRunning(sessionId) {
22542
+ return this.companionSessions.has(sessionId);
22543
+ }
21935
22544
  get activeCount() {
21936
22545
  return this.running.size;
21937
22546
  }
@@ -22120,9 +22729,9 @@ ${assistantSystemPrompt}` : assistantSystemPrompt;
22120
22729
  const copied = [];
22121
22730
  const linked = [];
22122
22731
  for (const entry of filesToCopy) {
22123
- const src = join17(repoPath, entry.path);
22732
+ const src = join18(repoPath, entry.path);
22124
22733
  if (!existsSync10(src)) continue;
22125
- const dst = join17(worktree.path, entry.path);
22734
+ const dst = join18(worktree.path, entry.path);
22126
22735
  await mkdir3(dirname6(dst), { recursive: true });
22127
22736
  try {
22128
22737
  if (entry.symlink) {
@@ -22542,6 +23151,8 @@ ${devSystemPromptResult.text}`;
22542
23151
  }
22543
23152
  const assistantSession = this.assistantSessions.get(streamId);
22544
23153
  if (assistantSession) return assistantSession.outputBuffer;
23154
+ const companionSession = this.companionSessions.get(streamId);
23155
+ if (companionSession) return companionSession.outputBuffer;
22545
23156
  return this.recentBuffers.get(streamId) ?? null;
22546
23157
  }
22547
23158
  resizeTerminal(streamId, cols, rows) {
@@ -22558,7 +23169,7 @@ ${devSystemPromptResult.text}`;
22558
23169
  for (const task of this.running.values()) {
22559
23170
  if (task.streamId === streamId) return task.process;
22560
23171
  }
22561
- return this.assistantSessions.get(streamId)?.process ?? this.liveProcesses.get(streamId);
23172
+ return this.assistantSessions.get(streamId)?.process ?? this.companionSessions.get(streamId)?.process ?? this.liveProcesses.get(streamId);
22562
23173
  }
22563
23174
  async handleHookEvent(event, taskId) {
22564
23175
  const { workspaceId, stateHub } = this.options;
@@ -22744,6 +23355,10 @@ ${devSystemPromptResult.text}`;
22744
23355
  }
22745
23356
  this.runningInstalls.clear();
22746
23357
  this.stopAssistantAgent();
23358
+ for (const [, task] of this.companionSessions) {
23359
+ task.process.kill();
23360
+ }
23361
+ this.companionSessions.clear();
22747
23362
  }
22748
23363
  // Call before stopAll() during graceful shutdown so onExit handlers bail out
22749
23364
  // and do not overwrite the failed/todo state written by cleanupStaleTasks().
@@ -22789,8 +23404,15 @@ function getMcpServerPath() {
22789
23404
  args: [resolve2(thisDir, "mcp-server.js")]
22790
23405
  };
22791
23406
  }
22792
- function buildAssistantAgentSystemPrompt(repoPath, secrets = [], systemPrompt) {
23407
+ function buildAssistantAgentSystemPrompt(repoPath, secrets = [], systemPrompt, resumedCanvas) {
22793
23408
  const secretsSection = buildSecretsSection(secrets);
23409
+ const resumedCanvasSection = resumedCanvas ? `
23410
+
23411
+ # Resuming a saved canvas
23412
+
23413
+ 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.
23414
+
23415
+ ${serializeCanvasBlocksForPrompt(resumedCanvas.blocks)}` : "";
22794
23416
  return `You are the Assistant for the project at \`${repoPath}\`.
22795
23417
 
22796
23418
  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 +23446,10 @@ You are a conversational project assistant. You can discuss the project, help pl
22824
23446
  - \`kanban_get_workflows\` \u2014 list all workflows (task and story/orch) with their agent slots, model tiers, tools, and prompts
22825
23447
  - \`kanban_upsert_workflow\` \u2014 create or fully replace a workflow (pass complete workflow object)
22826
23448
 
23449
+ ## Canvas
23450
+ - \`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
23451
+ - \`whipped_save_canvas\` \u2014 consolidate the conversation's canvas versions into one and save it to the reusable canvas library
23452
+
22827
23453
  ## Memory
22828
23454
  - \`whipped_search_memory\` \u2014 search durable project + global memory before re-discovering how something works
22829
23455
  - \`whipped_get_memory\` \u2014 fetch one memory's full content by id
@@ -22832,6 +23458,12 @@ You are a conversational project assistant. You can discuss the project, help pl
22832
23458
 
22833
23459
  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
23460
 
23461
+ # Sharing a canvas
23462
+
23463
+ 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.
23464
+
23465
+ ${buildCanvasModeGuidance()}${resumedCanvasSection}
23466
+
22835
23467
  # Card types
22836
23468
 
22837
23469
  **task** \u2014 a normal development ticket. Runs an optional plan slot, then dev, then any number of review slots (based on workflow).
@@ -25379,6 +26011,7 @@ var errorHandler2 = (err, c) => {
25379
26011
  };
25380
26012
 
25381
26013
  // src/api/routes/agent.ts
26014
+ init_api_contract();
25382
26015
  import { z as z3 } from "zod";
25383
26016
 
25384
26017
  // node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/utils/cookie.js
@@ -25676,8 +26309,8 @@ var zv = (target, schema) => zValidator(target, schema, (result) => {
25676
26309
  });
25677
26310
 
25678
26311
  // src/api/services/agent-service.ts
25679
- var startAgentSession = async (scheduler) => ({
25680
- taskId: await scheduler.startAssistantAgent()
26312
+ var startAgentSession = async (scheduler, override, savedCanvasId) => ({
26313
+ taskId: await scheduler.startAssistantAgent(override, savedCanvasId)
25681
26314
  });
25682
26315
  var stopAgentSession = async (scheduler) => {
25683
26316
  scheduler?.stopAssistantAgent();
@@ -25691,21 +26324,26 @@ var getAgentSessionStatus = async (scheduler) => {
25691
26324
  };
25692
26325
 
25693
26326
  // src/api/routes/agent.ts
26327
+ var startSessionBodySchema = z3.object({
26328
+ workspaceId: z3.string(),
26329
+ override: agentModelChoiceSchema.optional(),
26330
+ savedCanvasId: z3.string().optional()
26331
+ });
25694
26332
  var agentController = new Hono2().get("/session", zv("query", z3.object({ workspaceId: z3.string() })), async (c) => {
25695
26333
  const ctx = c.var.ctx;
25696
26334
  const { workspaceId } = c.req.valid("query");
25697
26335
  return c.json(await getAgentSessionStatus(ctx.getScheduler(workspaceId)));
25698
- }).post("/session", zv("json", z3.object({ workspaceId: z3.string() })), async (c) => {
26336
+ }).post("/session", zv("json", startSessionBodySchema), async (c) => {
25699
26337
  const ctx = c.var.ctx;
25700
- const { workspaceId } = c.req.valid("json");
26338
+ const { workspaceId, override, savedCanvasId } = c.req.valid("json");
25701
26339
  const scheduler = ctx.getScheduler(workspaceId);
25702
26340
  if (!scheduler) {
25703
26341
  await ctx.ensureWorkspace(workspaceId);
25704
26342
  const retried = ctx.getScheduler(workspaceId);
25705
26343
  if (!retried) throw NotFoundError("Workspace");
25706
- return c.json(await startAgentSession(retried));
26344
+ return c.json(await startAgentSession(retried, override, savedCanvasId));
25707
26345
  }
25708
- return c.json(await startAgentSession(scheduler));
26346
+ return c.json(await startAgentSession(scheduler, override, savedCanvasId));
25709
26347
  }).delete("/session", zv("query", z3.object({ workspaceId: z3.string() })), async (c) => {
25710
26348
  const ctx = c.var.ctx;
25711
26349
  const { workspaceId } = c.req.valid("query");
@@ -26251,8 +26889,8 @@ var getDiffService = async (workspaceId, cardId) => {
26251
26889
  const card = board.cards[cardId];
26252
26890
  if (!card) throw NotFoundError("Card");
26253
26891
  const worktreePath = getWorktreePath(resolveWorktreeOwnerId(cardId, board.cards));
26254
- const { existsSync: existsSync15 } = await import("node:fs");
26255
- if (!existsSync15(worktreePath)) {
26892
+ const { existsSync: existsSync16 } = await import("node:fs");
26893
+ if (!existsSync16(worktreePath)) {
26256
26894
  return { diff: null, error: "No worktree \u2014 agent has not started yet" };
26257
26895
  }
26258
26896
  const committedResult = spawnSync6("git", ["diff", `${card.baseRef}...HEAD`, "--no-color", "-U3"], {
@@ -26278,10 +26916,10 @@ var getDiffService = async (workspaceId, cardId) => {
26278
26916
  encoding: "utf-8"
26279
26917
  });
26280
26918
  const untrackedFiles = (untrackedResult.stdout ?? "").split("\n").map((f2) => f2.trim()).filter(Boolean);
26281
- const { readFileSync: readFileSync9 } = await import("node:fs");
26919
+ const { readFileSync: readFileSync10 } = await import("node:fs");
26282
26920
  const untrackedDiffs = untrackedFiles.map((file) => {
26283
26921
  try {
26284
- const content = readFileSync9(`${worktreePath}/${file}`, "utf-8");
26922
+ const content = readFileSync10(`${worktreePath}/${file}`, "utf-8");
26285
26923
  const lines = content.split("\n");
26286
26924
  const addedLines = lines.map((l, _i) => `+${l}`).join("\n");
26287
26925
  const hunkHeader = `@@ -0,0 +1,${lines.length} @@`;
@@ -26311,8 +26949,8 @@ var getCommitsService = async (workspaceId, cardId) => {
26311
26949
  const card = board.cards[cardId];
26312
26950
  if (!card) throw NotFoundError("Card");
26313
26951
  const worktreePath = getWorktreePath(resolveWorktreeOwnerId(cardId, board.cards));
26314
- const { existsSync: existsSync15 } = await import("node:fs");
26315
- if (!existsSync15(worktreePath)) return { commits: [] };
26952
+ const { existsSync: existsSync16 } = await import("node:fs");
26953
+ if (!existsSync16(worktreePath)) return { commits: [] };
26316
26954
  const result = spawnSync6("git", ["log", "--pretty=format:%H%x00%h%x00%s%x00%an%x00%ai", `${card.baseRef}..HEAD`], {
26317
26955
  cwd: worktreePath,
26318
26956
  encoding: "utf-8"
@@ -26333,8 +26971,8 @@ var getDiffForCommitService = async (workspaceId, cardId, commitHash) => {
26333
26971
  if (!card) throw NotFoundError("Card");
26334
26972
  if (!/^[0-9a-f]{4,64}$/i.test(commitHash)) return { diff: null, error: "Invalid commit hash" };
26335
26973
  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" };
26974
+ const { existsSync: existsSync16 } = await import("node:fs");
26975
+ if (!existsSync16(worktreePath)) return { diff: null, error: "No worktree" };
26338
26976
  const result = spawnSync6("git", ["show", commitHash, "--format=", "--patch", "--no-color", "-U3"], {
26339
26977
  cwd: worktreePath,
26340
26978
  encoding: "utf-8",
@@ -26582,6 +27220,376 @@ var cardsController = new Hono2().post("/", zv("json", runtimeCardCreateRequestS
26582
27220
  }
26583
27221
  );
26584
27222
 
27223
+ // src/api/routes/companion-saved-canvases.ts
27224
+ import { z as z6 } from "zod";
27225
+
27226
+ // src/api/services/companion-saved-canvases-service.ts
27227
+ async function listCompanionSavedCanvasesEntry(workspaceId) {
27228
+ return { canvases: listCompanionSavedCanvases(workspaceId) };
27229
+ }
27230
+ async function deleteCompanionSavedCanvasEntry(id) {
27231
+ deleteCompanionSavedCanvas(id);
27232
+ }
27233
+ async function clearCompanionCanvasesEntry(sessionId) {
27234
+ deleteCompanionCanvasesForSession(sessionId);
27235
+ }
27236
+ async function saveCompanionCanvasEntry(sessionId, workspaceId, title, blocks) {
27237
+ const session = getCompanionSession(sessionId);
27238
+ if (session) {
27239
+ if (session.savedCanvasId) {
27240
+ const updated = updateCompanionSavedCanvas(session.savedCanvasId, { title, blocks });
27241
+ if (updated) return updated;
27242
+ }
27243
+ const created = createCompanionSavedCanvas(workspaceId, { title, blocks, sourceSessionId: sessionId });
27244
+ setCompanionSessionSavedCanvasId(sessionId, created.id);
27245
+ return created;
27246
+ }
27247
+ const existing = findCompanionSavedCanvasBySourceSession(sessionId);
27248
+ if (existing) {
27249
+ const updated = updateCompanionSavedCanvas(existing.id, { title, blocks });
27250
+ if (updated) return updated;
27251
+ }
27252
+ return createCompanionSavedCanvas(workspaceId, { title, blocks, sourceSessionId: sessionId });
27253
+ }
27254
+
27255
+ // src/api/routes/companion-saved-canvases.ts
27256
+ var companionSavedCanvasesController = new Hono2().get("/", zv("query", z6.object({ workspaceId: z6.string() })), async (c) => {
27257
+ const { workspaceId } = c.req.valid("query");
27258
+ return c.json(await listCompanionSavedCanvasesEntry(workspaceId));
27259
+ }).delete("/:id", zv("param", z6.object({ id: z6.string() })), async (c) => {
27260
+ const { id } = c.req.valid("param");
27261
+ await deleteCompanionSavedCanvasEntry(id);
27262
+ return c.json({ ok: true });
27263
+ });
27264
+
27265
+ // src/api/routes/companion-sessions.ts
27266
+ init_api_contract();
27267
+ import { z as z7 } from "zod";
27268
+
27269
+ // src/api/services/companion-canvases-service.ts
27270
+ var createCompanionCanvasEntry = async (sessionId, workspaceId, blocks) => {
27271
+ return createCompanionCanvas(sessionId, workspaceId, blocks);
27272
+ };
27273
+ var listCompanionCanvasesEntry = async (sessionId) => {
27274
+ return { canvases: listCompanionCanvases(sessionId) };
27275
+ };
27276
+
27277
+ // src/api/services/companion-diff-service.ts
27278
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "node:fs";
27279
+ import { spawnSync as spawnSync7 } from "node:child_process";
27280
+ var MAX_BUFFER = 4 * 1024 * 1024;
27281
+ var getCompanionDiffService = async (id) => {
27282
+ const session = getCompanionSession(id);
27283
+ if (!session) throw NotFoundError("Companion session");
27284
+ if (!session.worktreePath || !existsSync11(session.worktreePath)) {
27285
+ return { diff: null, error: "No worktree \u2014 agent has not started yet" };
27286
+ }
27287
+ const worktreePath = session.worktreePath;
27288
+ const committedResult = spawnSync7("git", ["diff", `${session.baseRef}...HEAD`, "--no-color", "-U3"], {
27289
+ cwd: worktreePath,
27290
+ encoding: "utf-8",
27291
+ maxBuffer: MAX_BUFFER
27292
+ });
27293
+ if (committedResult.status !== 0 && committedResult.stderr) {
27294
+ return { diff: null, error: committedResult.stderr.trim() };
27295
+ }
27296
+ const stagedResult = spawnSync7("git", ["diff", "--cached", "--no-color", "-U3"], {
27297
+ cwd: worktreePath,
27298
+ encoding: "utf-8",
27299
+ maxBuffer: MAX_BUFFER
27300
+ });
27301
+ const unstagedResult = spawnSync7("git", ["diff", "--no-color", "-U3"], {
27302
+ cwd: worktreePath,
27303
+ encoding: "utf-8",
27304
+ maxBuffer: MAX_BUFFER
27305
+ });
27306
+ const untrackedResult = spawnSync7("git", ["ls-files", "--others", "--exclude-standard"], {
27307
+ cwd: worktreePath,
27308
+ encoding: "utf-8"
27309
+ });
27310
+ const untrackedFiles = (untrackedResult.stdout ?? "").split("\n").map((f2) => f2.trim()).filter(Boolean);
27311
+ const untrackedDiffs = untrackedFiles.map((file) => {
27312
+ try {
27313
+ const content = readFileSync7(`${worktreePath}/${file}`, "utf-8");
27314
+ const lines = content.split("\n");
27315
+ const addedLines = lines.map((l) => `+${l}`).join("\n");
27316
+ const hunkHeader = `@@ -0,0 +1,${lines.length} @@`;
27317
+ return `diff --git a/${file} b/${file}
27318
+ new file mode 100644
27319
+ --- /dev/null
27320
+ +++ b/${file}
27321
+ ${hunkHeader}
27322
+ ${addedLines}`;
27323
+ } catch {
27324
+ return null;
27325
+ }
27326
+ }).filter((d) => d !== null);
27327
+ const diff = [committedResult.stdout, stagedResult.stdout, unstagedResult.stdout, ...untrackedDiffs].filter((s2) => s2?.trim()).join("\n");
27328
+ const behindResult = spawnSync7("git", ["rev-list", "--count", `HEAD..${session.baseRef}`], {
27329
+ cwd: worktreePath,
27330
+ encoding: "utf-8"
27331
+ });
27332
+ const baseBehindCount = parseInt(behindResult.stdout?.trim() ?? "0", 10) || 0;
27333
+ return { diff, error: null, baseBehindCount };
27334
+ };
27335
+ var getCompanionCommitsService = async (id) => {
27336
+ const session = getCompanionSession(id);
27337
+ if (!session) throw NotFoundError("Companion session");
27338
+ if (!session.worktreePath || !existsSync11(session.worktreePath)) return { commits: [] };
27339
+ const result = spawnSync7("git", ["log", "--pretty=format:%H%x00%h%x00%s%x00%an%x00%ai", `${session.baseRef}..HEAD`], {
27340
+ cwd: session.worktreePath,
27341
+ encoding: "utf-8"
27342
+ });
27343
+ if (result.status !== 0 || !result.stdout?.trim()) return { commits: [] };
27344
+ const commits = result.stdout.trim().split("\n").filter(Boolean).map((line) => {
27345
+ const [hash = "", shortHash = "", message = "", author = "", date2 = ""] = line.split("\0");
27346
+ return { hash, shortHash, message, author, date: date2 };
27347
+ });
27348
+ return { commits };
27349
+ };
27350
+ var getCompanionDiffForCommitService = async (id, commitHash) => {
27351
+ const session = getCompanionSession(id);
27352
+ if (!session) throw NotFoundError("Companion session");
27353
+ if (!/^[0-9a-f]{4,64}$/i.test(commitHash)) return { diff: null, error: "Invalid commit hash" };
27354
+ if (!session.worktreePath || !existsSync11(session.worktreePath)) return { diff: null, error: "No worktree" };
27355
+ const result = spawnSync7("git", ["show", commitHash, "--format=", "--patch", "--no-color", "-U3"], {
27356
+ cwd: session.worktreePath,
27357
+ encoding: "utf-8",
27358
+ maxBuffer: MAX_BUFFER
27359
+ });
27360
+ if (result.status !== 0) return { diff: null, error: result.stderr?.trim() || "git show failed" };
27361
+ return { diff: result.stdout, error: null };
27362
+ };
27363
+
27364
+ // src/api/services/companion-merge-service.ts
27365
+ init_workspace_state();
27366
+ async function commitAndMergeCompanionService(id, repoPath, commitMessage, scheduler) {
27367
+ const session = getCompanionSession(id);
27368
+ if (!session) throw NotFoundError("Companion session");
27369
+ if (!session.useWorktree || !session.branchName) {
27370
+ throw BadRequestError("This session works directly in the main repo \u2014 there's nothing to merge");
27371
+ }
27372
+ if (!session.worktreePath) throw BadRequestError("Session has no worktree to merge");
27373
+ await scheduler?.stopCompanionAgent(id);
27374
+ const dirty = await isWorktreeDirty(session.worktreePath);
27375
+ if (dirty) {
27376
+ if (!commitMessage) return { status: "needs_commit" };
27377
+ await commitWorktree(session.worktreePath, commitMessage);
27378
+ }
27379
+ const result = attemptMerge(repoPath, id, session.branchName);
27380
+ setCompanionSessionWorktreePath(id, null);
27381
+ if (result.dirtyBase) {
27382
+ setCompanionSessionStatus(id, "stopped");
27383
+ return { status: "dirty_base" };
27384
+ }
27385
+ if (!result.ok) {
27386
+ abortMerge(repoPath);
27387
+ setCompanionSessionStatus(id, "stopped");
27388
+ return { status: "conflict", conflictedFiles: result.conflictedFiles };
27389
+ }
27390
+ setCompanionSessionStatus(id, "merged");
27391
+ return { status: "merged" };
27392
+ }
27393
+ async function commitAndPRCompanionService(id, workspaceId, commitMessage, title, description, baseRefOverride) {
27394
+ const session = getCompanionSession(id);
27395
+ if (!session) throw NotFoundError("Companion session");
27396
+ if (!session.worktreePath) throw BadRequestError("Session has no worktree");
27397
+ const projectConfig = await loadProjectConfig(workspaceId);
27398
+ const token = projectConfig.secrets?.find((s2) => s2.key === "GITHUB_TOKEN")?.value;
27399
+ if (!token) return { status: "no_token" };
27400
+ const dirty = await isWorktreeDirty(session.worktreePath);
27401
+ if (dirty) {
27402
+ if (!commitMessage) return { status: "needs_commit" };
27403
+ await commitWorktree(session.worktreePath, commitMessage);
27404
+ }
27405
+ const branch = session.branchName ?? getCurrentBranch(session.worktreePath);
27406
+ if (!branch) throw BadRequestError("Could not determine the current branch to push");
27407
+ await pushBranch(session.worktreePath, branch);
27408
+ const prUrl = await createGithubPR(
27409
+ session.worktreePath,
27410
+ title,
27411
+ description,
27412
+ baseRefOverride || session.baseRef,
27413
+ token
27414
+ );
27415
+ return { status: "pr_created", prUrl };
27416
+ }
27417
+
27418
+ // src/api/services/companion-service.ts
27419
+ init_api_contract();
27420
+ init_workspace_state();
27421
+ async function listCompanionSessionsEntry(workspaceId) {
27422
+ return listCompanionSessions(workspaceId);
27423
+ }
27424
+ function getCompanionSessionEntry(id) {
27425
+ return getCompanionSession(id);
27426
+ }
27427
+ async function createCompanionSessionEntry(workspaceId, repoPath, req, scheduler) {
27428
+ const useWorktree = req.useWorktree;
27429
+ const branchName = req.branchName?.trim() || null;
27430
+ if (useWorktree && !branchName) {
27431
+ throw BadRequestError("Branch name is required when using an isolated worktree");
27432
+ }
27433
+ const projectConfig = await loadProjectConfig(workspaceId);
27434
+ const workflow = req.workflowId ? projectConfig.workflows.find((w2) => w2.id === req.workflowId) : void 0;
27435
+ const devSlot = workflow?.slots.find((s2) => s2.type === "dev");
27436
+ const seedPrompt = devSlot ? resolvePromptText(devSlot.prompt, repoPath) : "";
27437
+ const suggestedPair = devSlot?.pairs[0];
27438
+ const model = req.model ?? (suggestedPair ? { agentId: suggestedPair.binary, model: suggestedPair.model, effort: suggestedPair.effort } : DEFAULT_AGENT_MODEL_CHOICE);
27439
+ const savedCanvas = req.savedCanvasId ? getCompanionSavedCanvas(req.savedCanvasId) : null;
27440
+ const name = req.name?.trim() || savedCanvas?.title || (useWorktree ? branchName : "Main repo session");
27441
+ const session = createCompanionSession(workspaceId, {
27442
+ name,
27443
+ useWorktree,
27444
+ baseRef: req.baseRef,
27445
+ branchName: useWorktree ? branchName : null,
27446
+ workflowId: workflow?.id ?? null,
27447
+ seedPrompt,
27448
+ agentId: model.agentId ?? "claude",
27449
+ model: model.model ?? null,
27450
+ effort: model.effort ?? null,
27451
+ savedCanvasId: savedCanvas?.id ?? null
27452
+ });
27453
+ if (savedCanvas) createCompanionCanvas(session.id, workspaceId, savedCanvas.blocks);
27454
+ await scheduler.startCompanionAgent(session);
27455
+ return getCompanionSession(session.id) ?? session;
27456
+ }
27457
+ async function stopCompanionSessionEntry(id, scheduler) {
27458
+ if (!getCompanionSession(id)) throw NotFoundError("Companion session");
27459
+ await scheduler?.stopCompanionAgent(id);
27460
+ }
27461
+ async function discardCompanionSessionEntry(id, repoPath, scheduler) {
27462
+ const session = getCompanionSession(id);
27463
+ if (!session) throw NotFoundError("Companion session");
27464
+ await scheduler?.stopCompanionAgent(id);
27465
+ if (session.useWorktree && session.worktreePath) {
27466
+ await removeWorktreeAsync(id, repoPath, session.branchName ?? void 0);
27467
+ }
27468
+ deleteCompanionSession(id);
27469
+ }
27470
+
27471
+ // src/api/routes/companion-sessions.ts
27472
+ var companionSessionsController = new Hono2().get("/", zv("query", z7.object({ workspaceId: z7.string() })), async (c) => {
27473
+ const { workspaceId } = c.req.valid("query");
27474
+ return c.json(await listCompanionSessionsEntry(workspaceId));
27475
+ }).get("/:id", zv("param", z7.object({ id: z7.string() })), async (c) => {
27476
+ const { id } = c.req.valid("param");
27477
+ const session = getCompanionSessionEntry(id);
27478
+ if (!session) throw NotFoundError("Companion session");
27479
+ return c.json(session);
27480
+ }).post("/", zv("json", companionSessionCreateRequestSchema.extend({ workspaceId: z7.string() })), async (c) => {
27481
+ const ctx = c.var.ctx;
27482
+ const { workspaceId, ...req } = c.req.valid("json");
27483
+ const ws = await ctx.ensureWorkspace(workspaceId);
27484
+ const scheduler = ctx.getScheduler(workspaceId);
27485
+ if (!scheduler) throw NotFoundError("Workspace");
27486
+ const session = await createCompanionSessionEntry(workspaceId, ws.repoPath, req, scheduler);
27487
+ return c.json(session);
27488
+ }).delete(
27489
+ "/:id",
27490
+ zv("param", z7.object({ id: z7.string() })),
27491
+ zv("query", z7.object({ workspaceId: z7.string() })),
27492
+ async (c) => {
27493
+ const ctx = c.var.ctx;
27494
+ const { id } = c.req.valid("param");
27495
+ const { workspaceId } = c.req.valid("query");
27496
+ await stopCompanionSessionEntry(id, ctx.getScheduler(workspaceId));
27497
+ return c.json({ ok: true });
27498
+ }
27499
+ ).post(
27500
+ "/:id/discard",
27501
+ zv("param", z7.object({ id: z7.string() })),
27502
+ zv("json", z7.object({ workspaceId: z7.string() })),
27503
+ async (c) => {
27504
+ const ctx = c.var.ctx;
27505
+ const { id } = c.req.valid("param");
27506
+ const { workspaceId } = c.req.valid("json");
27507
+ const ws = await ctx.ensureWorkspace(workspaceId);
27508
+ await discardCompanionSessionEntry(id, ws.repoPath, ctx.getScheduler(workspaceId));
27509
+ return c.json({ ok: true });
27510
+ }
27511
+ ).get("/:id/diff", zv("param", z7.object({ id: z7.string() })), async (c) => {
27512
+ const { id } = c.req.valid("param");
27513
+ return c.json(await getCompanionDiffService(id));
27514
+ }).get("/:id/commits", zv("param", z7.object({ id: z7.string() })), async (c) => {
27515
+ const { id } = c.req.valid("param");
27516
+ return c.json(await getCompanionCommitsService(id));
27517
+ }).get(
27518
+ "/:id/diff-for-commit",
27519
+ zv("param", z7.object({ id: z7.string() })),
27520
+ zv("query", z7.object({ commitHash: z7.string() })),
27521
+ async (c) => {
27522
+ const { id } = c.req.valid("param");
27523
+ const { commitHash } = c.req.valid("query");
27524
+ return c.json(await getCompanionDiffForCommitService(id, commitHash));
27525
+ }
27526
+ ).post(
27527
+ "/:id/commit-and-merge",
27528
+ zv("param", z7.object({ id: z7.string() })),
27529
+ zv("json", z7.object({ workspaceId: z7.string(), commitMessage: z7.string().optional() })),
27530
+ async (c) => {
27531
+ const ctx = c.var.ctx;
27532
+ const { id } = c.req.valid("param");
27533
+ const { workspaceId, commitMessage } = c.req.valid("json");
27534
+ const ws = await ctx.ensureWorkspace(workspaceId);
27535
+ const result = await commitAndMergeCompanionService(
27536
+ id,
27537
+ ws.repoPath,
27538
+ commitMessage,
27539
+ ctx.getScheduler(workspaceId)
27540
+ );
27541
+ return c.json(result);
27542
+ }
27543
+ ).post(
27544
+ "/:id/commit-and-pr",
27545
+ zv("param", z7.object({ id: z7.string() })),
27546
+ zv(
27547
+ "json",
27548
+ z7.object({
27549
+ workspaceId: z7.string(),
27550
+ commitMessage: z7.string().optional(),
27551
+ title: z7.string(),
27552
+ description: z7.string(),
27553
+ baseRef: z7.string().optional()
27554
+ })
27555
+ ),
27556
+ async (c) => {
27557
+ const { id } = c.req.valid("param");
27558
+ const { workspaceId, commitMessage, title, description, baseRef } = c.req.valid("json");
27559
+ const result = await commitAndPRCompanionService(id, workspaceId, commitMessage, title, description, baseRef);
27560
+ return c.json(result);
27561
+ }
27562
+ ).post(
27563
+ "/:id/canvas",
27564
+ zv("param", z7.object({ id: z7.string() })),
27565
+ zv("json", z7.object({ workspaceId: z7.string(), blocks: z7.array(canvasBlockSchema) })),
27566
+ async (c) => {
27567
+ const ctx = c.var.ctx;
27568
+ const { id } = c.req.valid("param");
27569
+ const { workspaceId, blocks } = c.req.valid("json");
27570
+ const canvas = await createCompanionCanvasEntry(id, workspaceId, blocks);
27571
+ ctx.stateHub.broadcastCompanionCanvasUpdate(workspaceId, id, canvas);
27572
+ return c.json(canvas);
27573
+ }
27574
+ ).get("/:id/canvases", zv("param", z7.object({ id: z7.string() })), async (c) => {
27575
+ const { id } = c.req.valid("param");
27576
+ return c.json(await listCompanionCanvasesEntry(id));
27577
+ }).delete("/:id/canvases", zv("param", z7.object({ id: z7.string() })), async (c) => {
27578
+ const { id } = c.req.valid("param");
27579
+ await clearCompanionCanvasesEntry(id);
27580
+ return c.json({ ok: true });
27581
+ }).post(
27582
+ "/:id/save-canvas",
27583
+ zv("param", z7.object({ id: z7.string() })),
27584
+ zv("json", z7.object({ workspaceId: z7.string(), title: z7.string(), blocks: z7.array(canvasBlockSchema) })),
27585
+ async (c) => {
27586
+ const { id } = c.req.valid("param");
27587
+ const { workspaceId, title, blocks } = c.req.valid("json");
27588
+ const saved = await saveCompanionCanvasEntry(id, workspaceId, title, blocks);
27589
+ return c.json(saved);
27590
+ }
27591
+ );
27592
+
26585
27593
  // src/api/routes/config.ts
26586
27594
  init_api_contract();
26587
27595
 
@@ -26605,20 +27613,20 @@ var configController = new Hono2().get("/", async (c) => {
26605
27613
  });
26606
27614
 
26607
27615
  // src/api/routes/fs.ts
26608
- import { z as z6 } from "zod";
27616
+ import { z as z8 } from "zod";
26609
27617
 
26610
27618
  // src/api/services/fs-service.ts
26611
27619
  init_runtime_config();
26612
- import { spawnSync as spawnSync8 } from "node:child_process";
26613
- import { existsSync as existsSync12, readdirSync as readdirSync2, statSync } from "node:fs";
27620
+ import { spawnSync as spawnSync9 } from "node:child_process";
27621
+ import { existsSync as existsSync13, readdirSync as readdirSync2, statSync } from "node:fs";
26614
27622
  import { homedir as homedir4 } from "node:os";
26615
- import { dirname as dirname7, join as join19, resolve as resolve3 } from "node:path";
27623
+ import { dirname as dirname7, join as join20, resolve as resolve3 } from "node:path";
26616
27624
 
26617
27625
  // src/core/terminal-apps.ts
26618
- import { spawnSync as spawnSync7 } from "node:child_process";
26619
- import { existsSync as existsSync11 } from "node:fs";
27626
+ import { spawnSync as spawnSync8 } from "node:child_process";
27627
+ import { existsSync as existsSync12 } from "node:fs";
26620
27628
  import { homedir as homedir3 } from "node:os";
26621
- import { join as join18 } from "node:path";
27629
+ import { join as join19 } from "node:path";
26622
27630
  var MACOS_TERMINALS = [
26623
27631
  { bundle: "Terminal", label: "Terminal" },
26624
27632
  { bundle: "iTerm", label: "iTerm" },
@@ -26650,13 +27658,13 @@ function appExists(bundle) {
26650
27658
  `/Applications/${bundle}.app`,
26651
27659
  `/System/Applications/${bundle}.app`,
26652
27660
  `/System/Applications/Utilities/${bundle}.app`,
26653
- join18(homedir3(), "Applications", `${bundle}.app`)
27661
+ join19(homedir3(), "Applications", `${bundle}.app`)
26654
27662
  ];
26655
- return paths.some((p2) => existsSync11(p2));
27663
+ return paths.some((p2) => existsSync12(p2));
26656
27664
  }
26657
27665
  function binaryExists(bin) {
26658
27666
  const finder = process.platform === "win32" ? "where" : "which";
26659
- const r = spawnSync7(finder, [bin], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" });
27667
+ const r = spawnSync8(finder, [bin], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" });
26660
27668
  return r.status === 0 && r.stdout.trim().length > 0;
26661
27669
  }
26662
27670
  function listTerminalApps() {
@@ -26671,26 +27679,26 @@ function listTerminalApps() {
26671
27679
  function openTerminalAt(path2, preferredId) {
26672
27680
  if (process.platform === "darwin") {
26673
27681
  const bundle = preferredId && appExists(preferredId) ? preferredId : "Terminal";
26674
- spawnSync7("open", ["-a", bundle, path2], { stdio: "ignore" });
27682
+ spawnSync8("open", ["-a", bundle, path2], { stdio: "ignore" });
26675
27683
  return;
26676
27684
  }
26677
27685
  if (process.platform === "win32") {
26678
27686
  const id = preferredId && WINDOWS_TERMINALS.some((t) => t.id === preferredId && binaryExists(t.check)) ? preferredId : "cmd";
26679
27687
  if (id === "wt") {
26680
- spawnSync7("wt", ["-d", path2], { stdio: "ignore" });
27688
+ spawnSync8("wt", ["-d", path2], { stdio: "ignore" });
26681
27689
  } else if (id === "powershell") {
26682
- spawnSync7("cmd", ["/c", "start", "powershell", "-NoExit", "-Command", `Set-Location -Path '${path2}'`], {
27690
+ spawnSync8("cmd", ["/c", "start", "powershell", "-NoExit", "-Command", `Set-Location -Path '${path2}'`], {
26683
27691
  stdio: "ignore"
26684
27692
  });
26685
27693
  } else {
26686
- spawnSync7("cmd", ["/c", "start", "cmd", "/K", `cd /D "${path2}"`], { stdio: "ignore" });
27694
+ spawnSync8("cmd", ["/c", "start", "cmd", "/K", `cd /D "${path2}"`], { stdio: "ignore" });
26687
27695
  }
26688
27696
  return;
26689
27697
  }
26690
27698
  const bin = preferredId && binaryExists(preferredId) ? preferredId : LINUX_TERMINALS.find((t) => binaryExists(t.bin))?.bin;
26691
27699
  if (!bin) return;
26692
27700
  const args = linuxLaunchArgs(bin, path2);
26693
- spawnSync7(bin, args, { stdio: "ignore" });
27701
+ spawnSync8(bin, args, { stdio: "ignore" });
26694
27702
  }
26695
27703
  function linuxLaunchArgs(bin, path2) {
26696
27704
  switch (bin) {
@@ -26716,7 +27724,7 @@ function linuxLaunchArgs(bin, path2) {
26716
27724
  // src/api/services/fs-service.ts
26717
27725
  var openPath = (path2) => {
26718
27726
  const cmd = process.platform === "win32" ? "explorer" : process.platform === "darwin" ? "open" : "xdg-open";
26719
- spawnSync8(cmd, [path2], { stdio: "ignore" });
27727
+ spawnSync9(cmd, [path2], { stdio: "ignore" });
26720
27728
  return { ok: true };
26721
27729
  };
26722
27730
  var listTerminals = async () => listTerminalApps();
@@ -26727,15 +27735,15 @@ var openTerminal = async (path2) => {
26727
27735
  };
26728
27736
  var listDir = async (path2, includeFiles, showHidden) => {
26729
27737
  let target = resolve3(path2 || homedir4());
26730
- while (target !== dirname7(target) && !(existsSync12(target) && statSync(target).isDirectory())) {
27738
+ while (target !== dirname7(target) && !(existsSync13(target) && statSync(target).isDirectory())) {
26731
27739
  target = dirname7(target);
26732
27740
  }
26733
27741
  const parent = dirname7(target);
26734
27742
  const visible = (name) => showHidden || !name.startsWith(".");
26735
27743
  try {
26736
27744
  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)) : [];
27745
+ 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));
27746
+ 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
27747
  return { current: target, parent: parent !== target ? parent : null, dirs, files };
26740
27748
  } catch {
26741
27749
  return { current: target, parent: parent !== target ? parent : null, dirs: [], files: [] };
@@ -26743,22 +27751,22 @@ var listDir = async (path2, includeFiles, showHidden) => {
26743
27751
  };
26744
27752
 
26745
27753
  // src/api/routes/fs.ts
26746
- var fsController = new Hono2().post("/open", zv("json", z6.object({ path: z6.string() })), async (c) => {
27754
+ var fsController = new Hono2().post("/open", zv("json", z8.object({ path: z8.string() })), async (c) => {
26747
27755
  const { path: path2 } = c.req.valid("json");
26748
27756
  return c.json(openPath(path2));
26749
27757
  }).get("/terminals", async (c) => {
26750
27758
  return c.json(await listTerminals());
26751
- }).post("/open-terminal", zv("json", z6.object({ path: z6.string() })), async (c) => {
27759
+ }).post("/open-terminal", zv("json", z8.object({ path: z8.string() })), async (c) => {
26752
27760
  const { path: path2 } = c.req.valid("json");
26753
27761
  return c.json(await openTerminal(path2));
26754
27762
  }).get(
26755
27763
  "/list-dir",
26756
27764
  zv(
26757
27765
  "query",
26758
- z6.object({
26759
- path: z6.string().optional().default(""),
26760
- includeFiles: z6.coerce.boolean().optional(),
26761
- showHidden: z6.coerce.boolean().optional()
27766
+ z8.object({
27767
+ path: z8.string().optional().default(""),
27768
+ includeFiles: z8.coerce.boolean().optional(),
27769
+ showHidden: z8.coerce.boolean().optional()
26762
27770
  })
26763
27771
  ),
26764
27772
  async (c) => {
@@ -26769,7 +27777,7 @@ var fsController = new Hono2().post("/open", zv("json", z6.object({ path: z6.str
26769
27777
 
26770
27778
  // src/api/routes/memory.ts
26771
27779
  init_api_contract();
26772
- import { z as z7 } from "zod";
27780
+ import { z as z9 } from "zod";
26773
27781
 
26774
27782
  // src/api/services/memory-service.ts
26775
27783
  var listMemoryEntries = async (filter) => listMemories(filter);
@@ -26801,9 +27809,9 @@ var memoryController = new Hono2().get(
26801
27809
  "/",
26802
27810
  zv(
26803
27811
  "query",
26804
- z7.object({
27812
+ z9.object({
26805
27813
  scope: memoryScopeSchema,
26806
- workspaceId: z7.string().optional(),
27814
+ workspaceId: z9.string().optional(),
26807
27815
  status: memoryStatusSchema.optional()
26808
27816
  })
26809
27817
  ),
@@ -26817,33 +27825,33 @@ var memoryController = new Hono2().get(
26817
27825
  })
26818
27826
  );
26819
27827
  }
26820
- ).get("/search", zv("query", z7.object({ query: z7.string(), workspaceId: z7.string().optional() })), async (c) => {
27828
+ ).get("/search", zv("query", z9.object({ query: z9.string(), workspaceId: z9.string().optional() })), async (c) => {
26821
27829
  const input = c.req.valid("query");
26822
27830
  return c.json(await searchMemoryEntries(input.query, input.workspaceId ?? null));
26823
- }).get("/for-card", zv("query", z7.object({ cardId: z7.string() })), async (c) => {
27831
+ }).get("/for-card", zv("query", z9.object({ cardId: z9.string() })), async (c) => {
26824
27832
  const input = c.req.valid("query");
26825
27833
  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) => {
27834
+ }).get("/tags", async (c) => c.json(await listTagsEntries())).get("/workspace-tags", zv("query", z9.object({ workspaceId: z9.string() })), async (c) => {
26827
27835
  const { workspaceId } = c.req.valid("query");
26828
27836
  return c.json(await getWorkspaceTagsEntry(workspaceId));
26829
- }).get("/:id", zv("param", z7.object({ id: z7.string() })), async (c) => {
27837
+ }).get("/:id", zv("param", z9.object({ id: z9.string() })), async (c) => {
26830
27838
  const { id } = c.req.valid("param");
26831
27839
  return c.json(await getMemoryEntry(id));
26832
27840
  }).post(
26833
27841
  "/propose",
26834
27842
  zv(
26835
27843
  "json",
26836
- z7.object({
27844
+ z9.object({
26837
27845
  scope: memoryScopeSchema,
26838
- workspaceId: z7.string().optional(),
26839
- originWorkspaceId: z7.string().optional(),
27846
+ workspaceId: z9.string().optional(),
27847
+ originWorkspaceId: z9.string().optional(),
26840
27848
  type: memoryTypeSchema,
26841
- title: z7.string().min(1),
26842
- content: z7.string().min(1),
27849
+ title: z9.string().min(1),
27850
+ content: z9.string().min(1),
26843
27851
  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(),
27852
+ importance: z9.number().int().min(1).max(3).optional(),
27853
+ tags: z9.array(z9.string()).optional(),
27854
+ originCardId: z9.string().optional(),
26847
27855
  originAgent: runtimeMemoryOriginAgentSchema.optional()
26848
27856
  })
26849
27857
  ),
@@ -26875,12 +27883,12 @@ var memoryController = new Hono2().get(
26875
27883
  "/propose-update",
26876
27884
  zv(
26877
27885
  "json",
26878
- z7.object({
26879
- id: z7.string(),
27886
+ z9.object({
27887
+ id: z9.string(),
26880
27888
  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(),
27889
+ title: z9.string().min(1).optional(),
27890
+ content: z9.string().min(1).optional(),
27891
+ importance: z9.number().int().min(1).max(3).optional(),
26884
27892
  sourceType: memorySourceTypeSchema.default("task_lesson")
26885
27893
  })
26886
27894
  ),
@@ -26894,16 +27902,16 @@ var memoryController = new Hono2().get(
26894
27902
  "/",
26895
27903
  zv(
26896
27904
  "json",
26897
- z7.object({
27905
+ z9.object({
26898
27906
  scope: memoryScopeSchema,
26899
- workspaceId: z7.string().optional(),
26900
- originWorkspaceId: z7.string().optional(),
27907
+ workspaceId: z9.string().optional(),
27908
+ originWorkspaceId: z9.string().optional(),
26901
27909
  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()
27910
+ title: z9.string().min(1),
27911
+ content: z9.string().min(1),
27912
+ importance: z9.number().int().min(1).max(3).optional(),
27913
+ tags: z9.array(z9.string()).optional(),
27914
+ boundWorkspaceIds: z9.array(z9.string()).optional()
26907
27915
  })
26908
27916
  ),
26909
27917
  async (c) => {
@@ -26932,14 +27940,14 @@ var memoryController = new Hono2().get(
26932
27940
  }
26933
27941
  ).patch(
26934
27942
  "/:id",
26935
- zv("param", z7.object({ id: z7.string() })),
27943
+ zv("param", z9.object({ id: z9.string() })),
26936
27944
  zv(
26937
27945
  "json",
26938
- z7.object({
27946
+ z9.object({
26939
27947
  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()
27948
+ title: z9.string().min(1).optional(),
27949
+ content: z9.string().min(1).optional(),
27950
+ importance: z9.number().int().min(1).max(3).optional()
26943
27951
  })
26944
27952
  ),
26945
27953
  async (c) => {
@@ -26948,19 +27956,19 @@ var memoryController = new Hono2().get(
26948
27956
  if (!updated) throw NotFoundError("Memory");
26949
27957
  return c.json(updated);
26950
27958
  }
26951
- ).post("/:id/approve", zv("param", z7.object({ id: z7.string() })), async (c) => {
27959
+ ).post("/:id/approve", zv("param", z9.object({ id: z9.string() })), async (c) => {
26952
27960
  const { id } = c.req.valid("param");
26953
27961
  const approved = await approveMemoryEntry(id);
26954
27962
  if (!approved) throw NotFoundError("Memory");
26955
27963
  return c.json(approved);
26956
- }).delete("/:id", zv("param", z7.object({ id: z7.string() })), async (c) => {
27964
+ }).delete("/:id", zv("param", z9.object({ id: z9.string() })), async (c) => {
26957
27965
  const { id } = c.req.valid("param");
26958
27966
  await removeMemoryEntry(id);
26959
27967
  return c.json({ ok: true });
26960
27968
  }).patch(
26961
27969
  "/:id/tags",
26962
- zv("param", z7.object({ id: z7.string() })),
26963
- zv("json", z7.object({ tags: z7.array(z7.string()) })),
27970
+ zv("param", z9.object({ id: z9.string() })),
27971
+ zv("json", z9.object({ tags: z9.array(z9.string()) })),
26964
27972
  async (c) => {
26965
27973
  const { id } = c.req.valid("param");
26966
27974
  const { tags } = c.req.valid("json");
@@ -26970,8 +27978,8 @@ var memoryController = new Hono2().get(
26970
27978
  }
26971
27979
  ).patch(
26972
27980
  "/:id/bindings",
26973
- zv("param", z7.object({ id: z7.string() })),
26974
- zv("json", z7.object({ workspaceIds: z7.array(z7.string()) })),
27981
+ zv("param", z9.object({ id: z9.string() })),
27982
+ zv("json", z9.object({ workspaceIds: z9.array(z9.string()) })),
26975
27983
  async (c) => {
26976
27984
  const { id } = c.req.valid("param");
26977
27985
  const { workspaceIds } = c.req.valid("json");
@@ -26979,7 +27987,7 @@ var memoryController = new Hono2().get(
26979
27987
  if (!updated) throw NotFoundError("Memory");
26980
27988
  return c.json(updated);
26981
27989
  }
26982
- ).put("/workspace-tags", zv("json", z7.object({ workspaceId: z7.string(), tags: z7.array(z7.string()) })), async (c) => {
27990
+ ).put("/workspace-tags", zv("json", z9.object({ workspaceId: z9.string(), tags: z9.array(z9.string()) })), async (c) => {
26983
27991
  const { workspaceId, tags } = c.req.valid("json");
26984
27992
  await setWorkspaceTagsEntry(workspaceId, tags);
26985
27993
  return c.json({ ok: true });
@@ -26987,7 +27995,7 @@ var memoryController = new Hono2().get(
26987
27995
 
26988
27996
  // src/api/routes/project-config.ts
26989
27997
  init_api_contract();
26990
- import { z as z8 } from "zod";
27998
+ import { z as z10 } from "zod";
26991
27999
 
26992
28000
  // src/api/services/project-config-service.ts
26993
28001
  init_workspace_state();
@@ -27007,21 +28015,21 @@ var setSystemPrompt = async (workspaceId, prompt) => {
27007
28015
  };
27008
28016
 
27009
28017
  // src/api/routes/project-config.ts
27010
- var projectConfigController = new Hono2().get("/", zv("query", z8.object({ workspaceId: z8.string() })), async (c) => {
28018
+ var projectConfigController = new Hono2().get("/", zv("query", z10.object({ workspaceId: z10.string() })), async (c) => {
27011
28019
  return c.json(await getProjectConfig(c.req.valid("query").workspaceId));
27012
- }).put("/", zv("json", z8.object({ workspaceId: z8.string(), config: runtimeProjectConfigSchema })), async (c) => {
28020
+ }).put("/", zv("json", z10.object({ workspaceId: z10.string(), config: runtimeProjectConfigSchema })), async (c) => {
27013
28021
  const ctx = c.var.ctx;
27014
28022
  const { workspaceId, config } = c.req.valid("json");
27015
28023
  await saveProjectConfig2(workspaceId, config);
27016
28024
  ctx.stateHub.broadcastWorkspaceUpdate(workspaceId);
27017
28025
  return c.json({ ok: true });
27018
- }).post("/git-instructions", zv("json", z8.object({ workspaceId: z8.string(), instructions: z8.string() })), async (c) => {
28026
+ }).post("/git-instructions", zv("json", z10.object({ workspaceId: z10.string(), instructions: z10.string() })), async (c) => {
27019
28027
  const ctx = c.var.ctx;
27020
28028
  const { workspaceId, instructions } = c.req.valid("json");
27021
28029
  const { cleared } = await setGitInstructions(workspaceId, instructions);
27022
28030
  ctx.stateHub.broadcastWorkspaceUpdate(workspaceId);
27023
28031
  return c.json({ ok: true, cleared });
27024
- }).post("/system-prompt", zv("json", z8.object({ workspaceId: z8.string(), prompt: z8.string() })), async (c) => {
28032
+ }).post("/system-prompt", zv("json", z10.object({ workspaceId: z10.string(), prompt: z10.string() })), async (c) => {
27025
28033
  const ctx = c.var.ctx;
27026
28034
  const { workspaceId, prompt } = c.req.valid("json");
27027
28035
  const { cleared } = await setSystemPrompt(workspaceId, prompt);
@@ -27031,12 +28039,12 @@ var projectConfigController = new Hono2().get("/", zv("query", z8.object({ works
27031
28039
 
27032
28040
  // src/api/routes/projects.ts
27033
28041
  init_api_contract();
27034
- import { z as z9 } from "zod";
28042
+ import { z as z11 } from "zod";
27035
28043
 
27036
28044
  // src/api/services/projects-service.ts
27037
28045
  init_runtime_config();
27038
28046
  init_api_contract();
27039
- import { spawnSync as spawnSync9 } from "node:child_process";
28047
+ import { spawnSync as spawnSync10 } from "node:child_process";
27040
28048
  init_logger();
27041
28049
 
27042
28050
  // src/state/projects-layout.ts
@@ -27119,7 +28127,7 @@ var checkProjectPath = async (repoPath) => {
27119
28127
  branches: []
27120
28128
  };
27121
28129
  }
27122
- const r = spawnSync9("git", ["rev-parse", "--git-dir"], {
28130
+ const r = spawnSync10("git", ["rev-parse", "--git-dir"], {
27123
28131
  cwd: repoPath,
27124
28132
  encoding: "utf-8",
27125
28133
  stdio: ["ignore", "pipe", "pipe"]
@@ -27135,12 +28143,12 @@ var checkProjectPath = async (repoPath) => {
27135
28143
  remote: null,
27136
28144
  branches: []
27137
28145
  };
27138
- const branchR = spawnSync9("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
28146
+ const branchR = spawnSync10("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
27139
28147
  cwd: repoPath,
27140
28148
  encoding: "utf-8",
27141
28149
  stdio: ["ignore", "pipe", "pipe"]
27142
28150
  });
27143
- const remoteR = spawnSync9("git", ["remote", "get-url", "origin"], {
28151
+ const remoteR = spawnSync10("git", ["remote", "get-url", "origin"], {
27144
28152
  cwd: repoPath,
27145
28153
  encoding: "utf-8",
27146
28154
  stdio: ["ignore", "pipe", "pipe"]
@@ -27159,7 +28167,7 @@ var addProject = async (repoPath, initialConfig) => {
27159
28167
  } catch {
27160
28168
  throw BadRequestError(`Path does not exist: ${repoPath}`);
27161
28169
  }
27162
- const r = spawnSync9("git", ["rev-parse", "--git-dir"], {
28170
+ const r = spawnSync10("git", ["rev-parse", "--git-dir"], {
27163
28171
  cwd: repoPath,
27164
28172
  encoding: "utf-8",
27165
28173
  stdio: ["ignore", "pipe", "pipe"]
@@ -27191,7 +28199,7 @@ var removeProject = async (workspaceId) => {
27191
28199
  const cards = boardCards;
27192
28200
  enqueueCleanup2(async () => {
27193
28201
  const { rm: rm3 } = await import("node:fs/promises");
27194
- const { join: join23 } = await import("node:path");
28202
+ const { join: join24 } = await import("node:path");
27195
28203
  for (const [cardId, card] of Object.entries(cards)) {
27196
28204
  if (resolveWorktreeOwnerId(cardId, cards) === cardId) {
27197
28205
  await removeWorktreeAsync(cardId, repoPath, card.branchName).catch((err) => {
@@ -27199,11 +28207,11 @@ var removeProject = async (workspaceId) => {
27199
28207
  });
27200
28208
  }
27201
28209
  }
27202
- await rm3(join23(WORKSPACES_DIR, workspaceId), { recursive: true, force: true }).catch((err) => {
28210
+ await rm3(join24(WORKSPACES_DIR, workspaceId), { recursive: true, force: true }).catch((err) => {
27203
28211
  logger.warn(`[cleanup:project:${workspaceId}] workspace dir failed: ${String(err)}`);
27204
28212
  });
27205
28213
  for (const cardId of Object.keys(cards)) {
27206
- await rm3(join23(ATTACHMENTS_DIR, cardId), { recursive: true, force: true }).catch(() => {
28214
+ await rm3(join24(ATTACHMENTS_DIR, cardId), { recursive: true, force: true }).catch(() => {
27207
28215
  });
27208
28216
  }
27209
28217
  logger.info(`[cleanup:project:${workspaceId}] done`);
@@ -27217,14 +28225,14 @@ var projectsController = new Hono2().get("/", async (c) => {
27217
28225
  return c.json(await listProjects());
27218
28226
  }).get("/layout", (c) => {
27219
28227
  return c.json(getProjectsLayout());
27220
- }).get("/check-path", zv("query", z9.object({ repoPath: z9.string() })), async (c) => {
28228
+ }).get("/check-path", zv("query", z11.object({ repoPath: z11.string() })), async (c) => {
27221
28229
  return c.json(await checkProjectPath(c.req.valid("query").repoPath));
27222
28230
  }).post(
27223
28231
  "/",
27224
28232
  zv(
27225
28233
  "json",
27226
- z9.object({
27227
- repoPath: z9.string().min(1),
28234
+ z11.object({
28235
+ repoPath: z11.string().min(1),
27228
28236
  initialConfig: runtimeProjectConfigSchema.partial().optional()
27229
28237
  })
27230
28238
  ),
@@ -27251,7 +28259,7 @@ var projectsController = new Hono2().get("/", async (c) => {
27251
28259
 
27252
28260
  // src/api/routes/recurring-agents.ts
27253
28261
  init_api_contract();
27254
- import { z as z10 } from "zod";
28262
+ import { z as z12 } from "zod";
27255
28263
 
27256
28264
  // src/api/services/recurring-agents-service.ts
27257
28265
  function listRecurringAgentsEntry(workspaceId) {
@@ -27276,20 +28284,20 @@ function setRecurringAgentJournalEntry(id, journal) {
27276
28284
  }
27277
28285
 
27278
28286
  // src/api/routes/recurring-agents.ts
27279
- var recurringAgentsController = new Hono2().get("/", zv("query", z10.object({ workspaceId: z10.string() })), async (c) => {
28287
+ var recurringAgentsController = new Hono2().get("/", zv("query", z12.object({ workspaceId: z12.string() })), async (c) => {
27280
28288
  const { workspaceId } = c.req.valid("query");
27281
28289
  return c.json(listRecurringAgentsEntry(workspaceId));
27282
- }).get("/:id", zv("param", z10.object({ id: z10.string() })), async (c) => {
28290
+ }).get("/:id", zv("param", z12.object({ id: z12.string() })), async (c) => {
27283
28291
  const { id } = c.req.valid("param");
27284
28292
  const agent = getRecurringAgentEntry(id);
27285
28293
  if (!agent) throw NotFoundError("Recurring agent");
27286
28294
  return c.json(agent);
27287
- }).post("/", zv("json", recurringAgentCreateRequestSchema.extend({ workspaceId: z10.string() })), async (c) => {
28295
+ }).post("/", zv("json", recurringAgentCreateRequestSchema.extend({ workspaceId: z12.string() })), async (c) => {
27288
28296
  const { workspaceId, ...req } = c.req.valid("json");
27289
28297
  return c.json(createRecurringAgentEntry(workspaceId, req));
27290
28298
  }).patch(
27291
28299
  "/:id",
27292
- zv("param", z10.object({ id: z10.string() })),
28300
+ zv("param", z12.object({ id: z12.string() })),
27293
28301
  zv("json", recurringAgentUpdateRequestSchema.omit({ id: true })),
27294
28302
  async (c) => {
27295
28303
  const { id } = c.req.valid("param");
@@ -27299,8 +28307,8 @@ var recurringAgentsController = new Hono2().get("/", zv("query", z10.object({ wo
27299
28307
  }
27300
28308
  ).post(
27301
28309
  "/:id/journal",
27302
- zv("param", z10.object({ id: z10.string() })),
27303
- zv("json", z10.object({ journal: z10.string() })),
28310
+ zv("param", z12.object({ id: z12.string() })),
28311
+ zv("json", z12.object({ journal: z12.string() })),
27304
28312
  async (c) => {
27305
28313
  const { id } = c.req.valid("param");
27306
28314
  const { journal } = c.req.valid("json");
@@ -27310,8 +28318,8 @@ var recurringAgentsController = new Hono2().get("/", zv("query", z10.object({ wo
27310
28318
  }
27311
28319
  ).post(
27312
28320
  "/:id/run",
27313
- zv("param", z10.object({ id: z10.string() })),
27314
- zv("query", z10.object({ workspaceId: z10.string() })),
28321
+ zv("param", z12.object({ id: z12.string() })),
28322
+ zv("query", z12.object({ workspaceId: z12.string() })),
27315
28323
  async (c) => {
27316
28324
  const { id } = c.req.valid("param");
27317
28325
  const { workspaceId } = c.req.valid("query");
@@ -27320,14 +28328,14 @@ var recurringAgentsController = new Hono2().get("/", zv("query", z10.object({ wo
27320
28328
  const started = await scheduler?.runNow(id) ?? false;
27321
28329
  return c.json({ started });
27322
28330
  }
27323
- ).delete("/:id", zv("param", z10.object({ id: z10.string() })), async (c) => {
28331
+ ).delete("/:id", zv("param", z12.object({ id: z12.string() })), async (c) => {
27324
28332
  const { id } = c.req.valid("param");
27325
28333
  deleteRecurringAgentEntry(id);
27326
28334
  return c.json({ ok: true });
27327
28335
  });
27328
28336
 
27329
28337
  // src/api/routes/run.ts
27330
- import { z as z11 } from "zod";
28338
+ import { z as z13 } from "zod";
27331
28339
 
27332
28340
  // src/api/services/run-service.ts
27333
28341
  init_workspace_state();
@@ -27343,15 +28351,19 @@ var resolveCardCwd = async (workspaceId, cardId, repoPath) => {
27343
28351
  if (!card) return null;
27344
28352
  return card.worktreePath ?? repoPath;
27345
28353
  };
28354
+ var resolveCompanionSessionCwd = (sessionId) => {
28355
+ const session = getCompanionSession(sessionId);
28356
+ return session?.worktreePath ?? null;
28357
+ };
27346
28358
 
27347
28359
  // src/api/routes/run.ts
27348
- var runController = new Hono2().get("/status", zv("query", z11.object({ workspaceId: z11.string() })), (c) => {
28360
+ var runController = new Hono2().get("/status", zv("query", z13.object({ workspaceId: z13.string() })), (c) => {
27349
28361
  const ctx = c.var.ctx;
27350
28362
  const { workspaceId } = c.req.valid("query");
27351
28363
  const session = ctx.getRunSession(workspaceId);
27352
28364
  if (!session) return c.json({ cardId: null, status: "stopped", errorMessage: void 0 });
27353
28365
  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) => {
28366
+ }).post("/start", zv("json", z13.object({ workspaceId: z13.string(), cardId: z13.string() })), async (c) => {
27355
28367
  const ctx = c.var.ctx;
27356
28368
  const { workspaceId, cardId } = c.req.valid("json");
27357
28369
  const ws = await ctx.ensureWorkspace(workspaceId);
@@ -27363,7 +28375,18 @@ var runController = new Hono2().get("/status", zv("query", z11.object({ workspac
27363
28375
  if (cwd === null) throw NotFoundError("Card");
27364
28376
  ctx.startRun(workspaceId, cardId, command, cwd);
27365
28377
  return c.json({ ok: true });
27366
- }).post("/start-base", zv("json", z11.object({ workspaceId: z11.string() })), async (c) => {
28378
+ }).post("/start-companion", zv("json", z13.object({ workspaceId: z13.string(), sessionId: z13.string() })), async (c) => {
28379
+ const ctx = c.var.ctx;
28380
+ const { workspaceId, sessionId } = c.req.valid("json");
28381
+ const command = await resolveStartCommand(workspaceId);
28382
+ if (!command) {
28383
+ throw PreconditionFailedError("No start command configured. Add one in Settings \u2192 Environment.");
28384
+ }
28385
+ const cwd = resolveCompanionSessionCwd(sessionId);
28386
+ if (cwd === null) throw NotFoundError("Companion session");
28387
+ ctx.startRun(workspaceId, sessionId, command, cwd);
28388
+ return c.json({ ok: true });
28389
+ }).post("/start-base", zv("json", z13.object({ workspaceId: z13.string() })), async (c) => {
27367
28390
  const ctx = c.var.ctx;
27368
28391
  const { workspaceId } = c.req.valid("json");
27369
28392
  const ws = await ctx.ensureWorkspace(workspaceId);
@@ -27373,7 +28396,7 @@ var runController = new Hono2().get("/status", zv("query", z11.object({ workspac
27373
28396
  }
27374
28397
  ctx.startRun(workspaceId, null, command, ws.repoPath);
27375
28398
  return c.json({ ok: true });
27376
- }).post("/stop", zv("json", z11.object({ workspaceId: z11.string() })), (c) => {
28399
+ }).post("/stop", zv("json", z13.object({ workspaceId: z13.string() })), (c) => {
27377
28400
  const ctx = c.var.ctx;
27378
28401
  const { workspaceId } = c.req.valid("json");
27379
28402
  ctx.stopRun(workspaceId);
@@ -27381,7 +28404,7 @@ var runController = new Hono2().get("/status", zv("query", z11.object({ workspac
27381
28404
  });
27382
28405
 
27383
28406
  // src/api/routes/slack.ts
27384
- import { z as z12 } from "zod";
28407
+ import { z as z14 } from "zod";
27385
28408
 
27386
28409
  // src/api/services/slack-service.ts
27387
28410
  init_runtime_config();
@@ -27428,20 +28451,20 @@ var createApp = async (appConfigToken, publicUrl, botName) => {
27428
28451
  var slackController = new Hono2().post("/resetApp", async (c) => {
27429
28452
  await resetApp();
27430
28453
  return c.json({ ok: true });
27431
- }).post("/updateSigningSecret", zv("json", z12.object({ signingSecret: z12.string().min(1) })), async (c) => {
28454
+ }).post("/updateSigningSecret", zv("json", z14.object({ signingSecret: z14.string().min(1) })), async (c) => {
27432
28455
  await updateSigningSecret(c.req.valid("json").signingSecret);
27433
28456
  return c.json({ ok: true });
27434
28457
  }).post(
27435
28458
  "/importCredentials",
27436
28459
  zv(
27437
28460
  "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()
28461
+ z14.object({
28462
+ slackAppId: z14.string(),
28463
+ slackClientId: z14.string(),
28464
+ slackClientSecret: z14.string(),
28465
+ slackSigningSecret: z14.string(),
28466
+ slackOauthAuthorizeUrl: z14.string(),
28467
+ slackPublicUrl: z14.string()
27445
28468
  })
27446
28469
  ),
27447
28470
  async (c) => {
@@ -27450,7 +28473,7 @@ var slackController = new Hono2().post("/resetApp", async (c) => {
27450
28473
  }
27451
28474
  ).post(
27452
28475
  "/createApp",
27453
- zv("json", z12.object({ appConfigToken: z12.string(), publicUrl: z12.string(), botName: z12.string().default("Whipped") })),
28476
+ zv("json", z14.object({ appConfigToken: z14.string(), publicUrl: z14.string(), botName: z14.string().default("Whipped") })),
27454
28477
  async (c) => {
27455
28478
  const { appConfigToken, publicUrl, botName } = c.req.valid("json");
27456
28479
  return c.json(await createApp(appConfigToken, publicUrl, botName));
@@ -27458,27 +28481,27 @@ var slackController = new Hono2().post("/resetApp", async (c) => {
27458
28481
  );
27459
28482
 
27460
28483
  // src/api/routes/terminal.ts
27461
- import { z as z13 } from "zod";
28484
+ import { z as z15 } from "zod";
27462
28485
 
27463
28486
  // src/api/services/terminal-service.ts
27464
28487
  var toBufferResponse = (buffer) => ({ data: buffer ?? "" });
27465
28488
 
27466
28489
  // src/api/routes/terminal.ts
27467
- var terminalController = new Hono2().get("/buffer", zv("query", z13.object({ workspaceId: z13.string(), taskId: z13.string() })), (c) => {
28490
+ var terminalController = new Hono2().get("/buffer", zv("query", z15.object({ workspaceId: z15.string(), taskId: z15.string() })), (c) => {
27468
28491
  const ctx = c.var.ctx;
27469
28492
  const { workspaceId, taskId } = c.req.valid("query");
27470
28493
  const buf = ctx.getScheduler(workspaceId)?.getOutputBuffer(taskId);
27471
28494
  return c.json(toBufferResponse(buf));
27472
28495
  }).post(
27473
28496
  "/resize",
27474
- zv("json", z13.object({ workspaceId: z13.string(), taskId: z13.string(), cols: z13.number(), rows: z13.number() })),
28497
+ zv("json", z15.object({ workspaceId: z15.string(), taskId: z15.string(), cols: z15.number(), rows: z15.number() })),
27475
28498
  (c) => {
27476
28499
  const ctx = c.var.ctx;
27477
28500
  const { workspaceId, taskId, cols, rows } = c.req.valid("json");
27478
28501
  ctx.getScheduler(workspaceId)?.resizeTerminal(taskId, cols, rows);
27479
28502
  return c.json({ ok: true });
27480
28503
  }
27481
- ).post("/input", zv("json", z13.object({ workspaceId: z13.string(), taskId: z13.string(), data: z13.string() })), (c) => {
28504
+ ).post("/input", zv("json", z15.object({ workspaceId: z15.string(), taskId: z15.string(), data: z15.string() })), (c) => {
27482
28505
  const ctx = c.var.ctx;
27483
28506
  const { workspaceId, taskId, data } = c.req.valid("json");
27484
28507
  ctx.getScheduler(workspaceId)?.writeToTerminal(taskId, data);
@@ -27486,7 +28509,7 @@ var terminalController = new Hono2().get("/buffer", zv("query", z13.object({ wor
27486
28509
  });
27487
28510
 
27488
28511
  // src/api/routes/tunnel.ts
27489
- import { z as z14 } from "zod";
28512
+ import { z as z16 } from "zod";
27490
28513
 
27491
28514
  // src/api/services/tunnel-service.ts
27492
28515
  init_runtime_config();
@@ -27495,12 +28518,12 @@ init_runtime_config();
27495
28518
  import { execFile as execFile9, spawn as spawn7 } from "node:child_process";
27496
28519
  import { mkdir as mkdir4, writeFile as writeFile3, readFile as readFile3, access, unlink as unlink4 } from "node:fs/promises";
27497
28520
  import { homedir as homedir5 } from "node:os";
27498
- import { join as join20 } from "node:path";
28521
+ import { join as join21 } from "node:path";
27499
28522
  import { promisify as promisify9 } from "node:util";
27500
28523
  init_runtime_config();
27501
28524
  init_logger();
27502
28525
  var execFileAsync7 = promisify9(execFile9);
27503
- var CLOUDFLARED_DIR = join20(homedir5(), ".cloudflared");
28526
+ var CLOUDFLARED_DIR = join21(homedir5(), ".cloudflared");
27504
28527
  async function checkCloudflaredInstalled() {
27505
28528
  try {
27506
28529
  const { stdout } = await execFileAsync7("cloudflared", ["--version"]);
@@ -27512,7 +28535,7 @@ async function checkCloudflaredInstalled() {
27512
28535
  }
27513
28536
  async function checkCloudflaredAuth() {
27514
28537
  try {
27515
- await access(join20(CLOUDFLARED_DIR, "cert.pem"));
28538
+ await access(join21(CLOUDFLARED_DIR, "cert.pem"));
27516
28539
  return true;
27517
28540
  } catch {
27518
28541
  return false;
@@ -27523,7 +28546,7 @@ async function openCloudflaredLogin(force = false) {
27523
28546
  if (alreadyLoggedIn && !force) return { alreadyLoggedIn: true };
27524
28547
  if (force) {
27525
28548
  try {
27526
- await unlink4(join20(CLOUDFLARED_DIR, "cert.pem"));
28549
+ await unlink4(join21(CLOUDFLARED_DIR, "cert.pem"));
27527
28550
  } catch {
27528
28551
  }
27529
28552
  }
@@ -27592,7 +28615,7 @@ async function createTunnel(tunnelName) {
27592
28615
  }
27593
28616
  }
27594
28617
  async function writeTunnelConfig(tunnelId, tunnelName, domain) {
27595
- const credentialsFile = join20(CLOUDFLARED_DIR, `${tunnelId}.json`);
28618
+ const credentialsFile = join21(CLOUDFLARED_DIR, `${tunnelId}.json`);
27596
28619
  const config = [
27597
28620
  `tunnel: ${tunnelId}`,
27598
28621
  `credentials-file: ${credentialsFile}`,
@@ -27603,12 +28626,12 @@ async function writeTunnelConfig(tunnelId, tunnelName, domain) {
27603
28626
  ` - service: http_status:404`
27604
28627
  ].join("\n");
27605
28628
  await mkdir4(CLOUDFLARED_DIR, { recursive: true });
27606
- await writeFile3(join20(CLOUDFLARED_DIR, "config.yml"), config, "utf-8");
28629
+ await writeFile3(join21(CLOUDFLARED_DIR, "config.yml"), config, "utf-8");
27607
28630
  logger.info(`[tunnel-setup] Wrote ~/.cloudflared/config.yml for tunnel ${tunnelName}`);
27608
28631
  }
27609
28632
  async function readTunnelConfig() {
27610
28633
  try {
27611
- const raw2 = await readFile3(join20(CLOUDFLARED_DIR, "config.yml"), "utf-8");
28634
+ const raw2 = await readFile3(join21(CLOUDFLARED_DIR, "config.yml"), "utf-8");
27612
28635
  const tunnelMatch = raw2.match(/^tunnel:\s*(.+)$/m);
27613
28636
  const hostnameMatch = raw2.match(/hostname:\s*(.+)$/m);
27614
28637
  return {
@@ -27657,9 +28680,9 @@ var resetTunnel = async () => {
27657
28680
  await updateGlobalConfig({ tunnelId: void 0, tunnelDomain: void 0, autoStartTunnel: false });
27658
28681
  const { unlink: unlink5 } = await import("node:fs/promises");
27659
28682
  const { homedir: homedir6 } = await import("node:os");
27660
- const { join: join23 } = await import("node:path");
28683
+ const { join: join24 } = await import("node:path");
27661
28684
  try {
27662
- await unlink5(join23(homedir6(), ".cloudflared", "config.yml"));
28685
+ await unlink5(join24(homedir6(), ".cloudflared", "config.yml"));
27663
28686
  } catch {
27664
28687
  }
27665
28688
  };
@@ -27671,9 +28694,9 @@ var tunnelController = new Hono2().get("/checkCloudflared", async (c) => {
27671
28694
  return c.json(await getTunnelConfig());
27672
28695
  }).get("/tunnelStatus", async (c) => {
27673
28696
  return c.json(await getTunnelStatus());
27674
- }).post("/cloudflaredLogin", zv("json", z14.object({ force: z14.boolean().default(false) })), async (c) => {
28697
+ }).post("/cloudflaredLogin", zv("json", z16.object({ force: z16.boolean().default(false) })), async (c) => {
27675
28698
  return c.json(await cloudflaredLogin(c.req.valid("json").force));
27676
- }).post("/createTunnel", zv("json", z14.object({ domain: z14.string() })), async (c) => {
28699
+ }).post("/createTunnel", zv("json", z16.object({ domain: z16.string() })), async (c) => {
27677
28700
  return c.json(await createTunnel2(c.req.valid("json").domain));
27678
28701
  }).post("/startTunnel", async (c) => {
27679
28702
  return c.json(await startTunnel());
@@ -27686,11 +28709,11 @@ var tunnelController = new Hono2().get("/checkCloudflared", async (c) => {
27686
28709
 
27687
28710
  // src/api/routes/workflows.ts
27688
28711
  init_api_contract();
27689
- import { z as z15 } from "zod";
28712
+ import { z as z17 } from "zod";
27690
28713
 
27691
28714
  // src/api/services/workflows-service.ts
27692
28715
  init_workspace_state();
27693
- import { existsSync as existsSync13 } from "node:fs";
28716
+ import { existsSync as existsSync14 } from "node:fs";
27694
28717
  import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
27695
28718
  import { dirname as dirname8, isAbsolute as isAbsolute2, resolve as resolve4 } from "node:path";
27696
28719
  var resolvePromptPath = async (workspaceId, requestedPath) => {
@@ -27735,22 +28758,22 @@ var writePromptFile = async (workspaceId, path2, content) => {
27735
28758
  };
27736
28759
  var readPromptFile = async (workspaceId, path2) => {
27737
28760
  const targetPath = await resolvePromptPath(workspaceId, path2);
27738
- if (!existsSync13(targetPath)) return { content: "", exists: false };
28761
+ if (!existsSync14(targetPath)) return { content: "", exists: false };
27739
28762
  const content = await readFile4(targetPath, "utf-8");
27740
28763
  return { content, exists: true };
27741
28764
  };
27742
28765
 
27743
28766
  // src/api/routes/workflows.ts
27744
- var workflowsController = new Hono2().get("/", zv("query", z15.object({ workspaceId: z15.string() })), async (c) => {
28767
+ var workflowsController = new Hono2().get("/", zv("query", z17.object({ workspaceId: z17.string() })), async (c) => {
27745
28768
  const { workspaceId } = c.req.valid("query");
27746
28769
  return c.json(await listWorkflows(workspaceId));
27747
- }).post("/", zv("json", z15.object({ workspaceId: z15.string(), workflow: workflowSchema })), async (c) => {
28770
+ }).post("/", zv("json", z17.object({ workspaceId: z17.string(), workflow: workflowSchema })), async (c) => {
27748
28771
  const ctx = c.var.ctx;
27749
28772
  const { workspaceId, workflow } = c.req.valid("json");
27750
28773
  const result = await upsertWorkflow(workspaceId, workflow);
27751
28774
  ctx.stateHub.broadcastWorkspaceUpdate(workspaceId);
27752
28775
  return c.json(result);
27753
- }).delete("/:workflowId", zv("query", z15.object({ workspaceId: z15.string() })), async (c) => {
28776
+ }).delete("/:workflowId", zv("query", z17.object({ workspaceId: z17.string() })), async (c) => {
27754
28777
  const ctx = c.var.ctx;
27755
28778
  const { workspaceId } = c.req.valid("query");
27756
28779
  const workflowId = c.req.param("workflowId");
@@ -27759,23 +28782,23 @@ var workflowsController = new Hono2().get("/", zv("query", z15.object({ workspac
27759
28782
  return c.json(result);
27760
28783
  }).post(
27761
28784
  "/prompt-file",
27762
- zv("json", z15.object({ workspaceId: z15.string(), path: z15.string().min(1), content: z15.string() })),
28785
+ zv("json", z17.object({ workspaceId: z17.string(), path: z17.string().min(1), content: z17.string() })),
27763
28786
  async (c) => {
27764
28787
  const { workspaceId, path: path2, content } = c.req.valid("json");
27765
28788
  return c.json(await writePromptFile(workspaceId, path2, content));
27766
28789
  }
27767
- ).get("/prompt-file", zv("query", z15.object({ workspaceId: z15.string(), path: z15.string().min(1) })), async (c) => {
28790
+ ).get("/prompt-file", zv("query", z17.object({ workspaceId: z17.string(), path: z17.string().min(1) })), async (c) => {
27768
28791
  const { workspaceId, path: path2 } = c.req.valid("query");
27769
28792
  return c.json(await readPromptFile(workspaceId, path2));
27770
28793
  });
27771
28794
 
27772
28795
  // src/api/routes/workspace.ts
27773
28796
  init_api_contract();
27774
- import { z as z16 } from "zod";
28797
+ import { z as z18 } from "zod";
27775
28798
 
27776
28799
  // src/api/services/workspace-service.ts
27777
28800
  init_workspace_state();
27778
- import { spawnSync as spawnSync10 } from "node:child_process";
28801
+ import { spawnSync as spawnSync11 } from "node:child_process";
27779
28802
  var loadStateForWorkspace = async (workspaceId) => {
27780
28803
  const workspaces = await listWorkspaces();
27781
28804
  const ws = workspaces.find((w2) => w2.workspaceId === workspaceId);
@@ -27785,12 +28808,12 @@ var loadStateForWorkspace = async (workspaceId) => {
27785
28808
  var loadStateForContext = async (workspaceId, repoPath) => loadWorkspaceState(workspaceId, repoPath);
27786
28809
  var saveState = async (workspaceId, request2) => saveWorkspaceState(workspaceId, request2);
27787
28810
  var listRootFiles = (repoPath) => {
27788
- const ignored = spawnSync10(
28811
+ const ignored = spawnSync11(
27789
28812
  "git",
27790
28813
  ["ls-files", "--others", "--ignored", "--exclude-standard", "--directory", "--no-empty-directory"],
27791
28814
  { cwd: repoPath, encoding: "utf-8" }
27792
28815
  );
27793
- const untracked = spawnSync10("git", ["ls-files", "--others", "--exclude-standard"], {
28816
+ const untracked = spawnSync11("git", ["ls-files", "--others", "--exclude-standard"], {
27794
28817
  cwd: repoPath,
27795
28818
  encoding: "utf-8"
27796
28819
  });
@@ -27799,7 +28822,7 @@ var listRootFiles = (repoPath) => {
27799
28822
  };
27800
28823
 
27801
28824
  // src/api/routes/workspace.ts
27802
- var workspaceController = new Hono2().get("/state", zv("query", z16.object({ workspaceId: z16.string().optional() })), async (c) => {
28825
+ var workspaceController = new Hono2().get("/state", zv("query", z18.object({ workspaceId: z18.string().optional() })), async (c) => {
27803
28826
  const ctx = c.var.ctx;
27804
28827
  const { workspaceId } = c.req.valid("query");
27805
28828
  if (workspaceId) {
@@ -27807,10 +28830,10 @@ var workspaceController = new Hono2().get("/state", zv("query", z16.object({ wor
27807
28830
  }
27808
28831
  if (!ctx.currentWorkspaceId || !ctx.currentRepoPath) throw BadRequestError("No workspace context");
27809
28832
  return c.json(await loadStateForContext(ctx.currentWorkspaceId, ctx.currentRepoPath));
27810
- }).post("/save", zv("json", runtimeWorkspaceStateSaveRequestSchema.extend({ workspaceId: z16.string() })), async (c) => {
28833
+ }).post("/save", zv("json", runtimeWorkspaceStateSaveRequestSchema.extend({ workspaceId: z18.string() })), async (c) => {
27811
28834
  const { workspaceId, board, revision } = c.req.valid("json");
27812
28835
  return c.json(await saveState(workspaceId, { board, revision }));
27813
- }).get("/root-files", zv("query", z16.object({ workspaceId: z16.string() })), async (c) => {
28836
+ }).get("/root-files", zv("query", z18.object({ workspaceId: z18.string() })), async (c) => {
27814
28837
  const ctx = c.var.ctx;
27815
28838
  const { workspaceId } = c.req.valid("query");
27816
28839
  const ws = await ctx.ensureWorkspace(workspaceId);
@@ -27822,7 +28845,7 @@ function createApiApp(ctx) {
27822
28845
  const app = new Hono2().basePath("/api").use("*", async (c, next) => {
27823
28846
  c.set("ctx", ctx);
27824
28847
  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);
28848
+ }).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
28849
  app.onError(errorHandler2);
27827
28850
  return app;
27828
28851
  }
@@ -27932,6 +28955,9 @@ var RuntimeStateHub = class {
27932
28955
  broadcastRunSessionChange(workspaceId, cardId, status, errorMessage) {
27933
28956
  this.broadcastToWorkspace(workspaceId, { type: "run_session_changed", cardId, status, errorMessage });
27934
28957
  }
28958
+ broadcastCompanionCanvasUpdate(workspaceId, sessionId, canvas) {
28959
+ this.broadcastToWorkspace(workspaceId, { type: "companion_canvas_updated", sessionId, canvas });
28960
+ }
27935
28961
  broadcastToWorkspace(workspaceId, event) {
27936
28962
  const clientIds = this.workspaceClients.get(workspaceId);
27937
28963
  if (!clientIds) return;
@@ -27952,14 +28978,14 @@ var RuntimeStateHub = class {
27952
28978
 
27953
28979
  // src/core/update-check.ts
27954
28980
  init_paths();
27955
- 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");
28981
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "node:fs";
28982
+ import { join as join22 } from "node:path";
28983
+ var CACHE_FILE = join22(WHIPPED_HOME_DIR, "update-check.json");
27958
28984
  var REGISTRY_URL = "https://registry.npmjs.org/whipped/latest";
27959
28985
  var CHECK_INTERVAL_MS = 12 * 60 * 60 * 1e3;
27960
28986
  function readCache() {
27961
28987
  try {
27962
- return JSON.parse(readFileSync7(CACHE_FILE, "utf8"));
28988
+ return JSON.parse(readFileSync8(CACHE_FILE, "utf8"));
27963
28989
  } catch {
27964
28990
  return null;
27965
28991
  }
@@ -28013,6 +29039,10 @@ async function cleanupStaleTasks(workspaceId, hub) {
28013
29039
  if (staleRecurring > 0) {
28014
29040
  logger.info(`[server] Cleared ${staleRecurring} stale recurring-agent run(s) for ${workspaceId}`);
28015
29041
  }
29042
+ const staleCompanions = resetStaleCompanionSessions(workspaceId);
29043
+ if (staleCompanions > 0) {
29044
+ logger.info(`[server] Reset ${staleCompanions} stale companion session(s) for ${workspaceId}`);
29045
+ }
28016
29046
  for (const card of Object.values(board.cards)) {
28017
29047
  if (card.terminalSessions?.some((s2) => s2.endedAt === void 0)) {
28018
29048
  await closeAllOpenTerminalSessions(workspaceId, card.id, now);
@@ -28241,9 +29271,9 @@ async function createRuntimeServer(options) {
28241
29271
  };
28242
29272
  }
28243
29273
  const apiApp = createApiApp(createContext());
28244
- const webUiDistPath = join22(__dirname2, "web-ui");
28245
- const webUiIndexPath = join22(webUiDistPath, "index.html");
28246
- const hasWebUi = existsSync14(webUiIndexPath);
29274
+ const webUiDistPath = join23(__dirname2, "web-ui");
29275
+ const webUiIndexPath = join23(webUiDistPath, "index.html");
29276
+ const hasWebUi = existsSync15(webUiIndexPath);
28247
29277
  const httpServer = createServer(async (req, res) => {
28248
29278
  const url = new URL(req.url ?? "/", `http://${host}`);
28249
29279
  if (url.pathname === "/api/slack/oauth-callback") {
@@ -28476,7 +29506,7 @@ async function createRuntimeServer(options) {
28476
29506
  res.end("Bad request");
28477
29507
  return;
28478
29508
  }
28479
- const filePath = join22(ATTACHMENTS_DIR, cardId, filename);
29509
+ const filePath = join23(ATTACHMENTS_DIR, cardId, filename);
28480
29510
  const { readFile: readFile5 } = await import("node:fs/promises");
28481
29511
  try {
28482
29512
  const data = await readFile5(filePath);
@@ -28551,15 +29581,15 @@ async function createRuntimeServer(options) {
28551
29581
  return;
28552
29582
  }
28553
29583
  if (hasWebUi) {
28554
- const filePath = url.pathname === "/" || !url.pathname.includes(".") ? webUiIndexPath : join22(webUiDistPath, url.pathname);
28555
- if (existsSync14(filePath)) {
28556
- const content = readFileSync8(filePath);
29584
+ const filePath = url.pathname === "/" || !url.pathname.includes(".") ? webUiIndexPath : join23(webUiDistPath, url.pathname);
29585
+ if (existsSync15(filePath)) {
29586
+ const content = readFileSync9(filePath);
28557
29587
  res.writeHead(200, { "Content-Type": getContentType(filePath) });
28558
29588
  res.end(content);
28559
29589
  return;
28560
29590
  }
28561
29591
  res.writeHead(200, { "Content-Type": "text/html" });
28562
- res.end(readFileSync8(webUiIndexPath));
29592
+ res.end(readFileSync9(webUiIndexPath));
28563
29593
  return;
28564
29594
  }
28565
29595
  res.writeHead(200, { "Content-Type": "text/plain" });
@@ -28632,15 +29662,17 @@ async function createRuntimeServer(options) {
28632
29662
  if (activeBuffer !== null) {
28633
29663
  if (activeBuffer && ws.readyState === 1) ws.send(prepareBufferForReplay(activeBuffer));
28634
29664
  } 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
- }
29665
+ loadTerminalBuffer(workspaceId, taskId).then((diskSnapshot) => {
29666
+ if (diskSnapshot) {
29667
+ if (ws.readyState === 1) ws.send(prepareBufferForReplay(diskSnapshot));
29668
+ return;
29669
+ }
29670
+ const hubSnapshot = stateHub.getTerminalBuffer(workspaceId, taskId);
29671
+ if (hubSnapshot && ws.readyState === 1) ws.send(prepareBufferForReplay(hubSnapshot));
29672
+ }).catch(() => {
29673
+ const hubSnapshot = stateHub.getTerminalBuffer(workspaceId, taskId);
29674
+ if (hubSnapshot && ws.readyState === 1) ws.send(prepareBufferForReplay(hubSnapshot));
29675
+ });
28644
29676
  }
28645
29677
  ws.on("message", (raw2) => {
28646
29678
  const text = raw2.toString();
@@ -28720,6 +29752,9 @@ async function createRuntimeServer(options) {
28720
29752
  await writeClaudeTaskHookSettings(port).catch((err) => {
28721
29753
  logger.warn("[server] Failed to write claude hook settings:", err);
28722
29754
  });
29755
+ await writeClaudeCompanionSettings().catch((err) => {
29756
+ logger.warn("[server] Failed to write claude companion settings:", err);
29757
+ });
28723
29758
  if (_globalConfig.autoStartTunnel) tunnelManager.start();
28724
29759
  scheduleUpdateChecks(VERSION10, (latestVersion) => {
28725
29760
  stateHub.broadcastUpdateAvailable(latestVersion);
@@ -28770,7 +29805,7 @@ process.on("uncaughtException", (err) => {
28770
29805
  if (err.code === "EPIPE" || err.code === "ECONNRESET") return;
28771
29806
  throw err;
28772
29807
  });
28773
- var VERSION9 = true ? "0.8.1" : "0.0.0-dev";
29808
+ var VERSION9 = true ? "0.9.0" : "0.0.0-dev";
28774
29809
  async function isPortAvailable(port, host) {
28775
29810
  return new Promise((resolve5) => {
28776
29811
  const probe = createServer2();