patchwork-os 0.2.0-alpha.3 → 0.2.0-alpha.30

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 (298) hide show
  1. package/README.bridge.md +6 -0
  2. package/README.md +40 -15
  3. package/deploy/bootstrap-vps.sh +184 -0
  4. package/deploy/deploy-dashboard.sh +174 -0
  5. package/deploy/deploy-landing.sh +79 -0
  6. package/dist/activationMetrics.d.ts +67 -0
  7. package/dist/activationMetrics.js +255 -0
  8. package/dist/activationMetrics.js.map +1 -0
  9. package/dist/approvalHttp.d.ts +24 -2
  10. package/dist/approvalHttp.js +150 -10
  11. package/dist/approvalHttp.js.map +1 -1
  12. package/dist/approvalQueue.d.ts +16 -1
  13. package/dist/approvalQueue.js +44 -3
  14. package/dist/approvalQueue.js.map +1 -1
  15. package/dist/automation.d.ts +20 -0
  16. package/dist/automation.js +54 -1
  17. package/dist/automation.js.map +1 -1
  18. package/dist/bridge.d.ts +7 -0
  19. package/dist/bridge.js +225 -35
  20. package/dist/bridge.js.map +1 -1
  21. package/dist/bridgeToken.js +57 -19
  22. package/dist/bridgeToken.js.map +1 -1
  23. package/dist/ccPermissions.js +6 -4
  24. package/dist/ccPermissions.js.map +1 -1
  25. package/dist/claudeOrchestrator.d.ts +1 -1
  26. package/dist/claudeOrchestrator.js +14 -8
  27. package/dist/claudeOrchestrator.js.map +1 -1
  28. package/dist/commands/launchd.d.ts +2 -0
  29. package/dist/commands/launchd.js +94 -0
  30. package/dist/commands/launchd.js.map +1 -0
  31. package/dist/commands/recipe.d.ts +258 -0
  32. package/dist/commands/recipe.js +1130 -0
  33. package/dist/commands/recipe.js.map +1 -0
  34. package/dist/commands/recipeInstall.d.ts +72 -0
  35. package/dist/commands/recipeInstall.js +339 -0
  36. package/dist/commands/recipeInstall.js.map +1 -0
  37. package/dist/config.d.ts +14 -1
  38. package/dist/config.js +99 -8
  39. package/dist/config.js.map +1 -1
  40. package/dist/connectors/baseConnector.d.ts +117 -0
  41. package/dist/connectors/baseConnector.js +213 -0
  42. package/dist/connectors/baseConnector.js.map +1 -0
  43. package/dist/connectors/confluence.d.ts +111 -0
  44. package/dist/connectors/confluence.js +406 -0
  45. package/dist/connectors/confluence.js.map +1 -0
  46. package/dist/connectors/datadog.d.ts +116 -0
  47. package/dist/connectors/datadog.js +385 -0
  48. package/dist/connectors/datadog.js.map +1 -0
  49. package/dist/connectors/fixtureLibrary.d.ts +21 -0
  50. package/dist/connectors/fixtureLibrary.js +70 -0
  51. package/dist/connectors/fixtureLibrary.js.map +1 -0
  52. package/dist/connectors/fixtureRecorder.d.ts +1 -0
  53. package/dist/connectors/fixtureRecorder.js +35 -0
  54. package/dist/connectors/fixtureRecorder.js.map +1 -0
  55. package/dist/connectors/github.d.ts +58 -8
  56. package/dist/connectors/github.js +312 -84
  57. package/dist/connectors/github.js.map +1 -1
  58. package/dist/connectors/gmail.d.ts +4 -1
  59. package/dist/connectors/gmail.js +79 -16
  60. package/dist/connectors/gmail.js.map +1 -1
  61. package/dist/connectors/googleCalendar.d.ts +60 -0
  62. package/dist/connectors/googleCalendar.js +345 -0
  63. package/dist/connectors/googleCalendar.js.map +1 -0
  64. package/dist/connectors/hubspot.d.ts +112 -0
  65. package/dist/connectors/hubspot.js +408 -0
  66. package/dist/connectors/hubspot.js.map +1 -0
  67. package/dist/connectors/intercom.d.ts +102 -0
  68. package/dist/connectors/intercom.js +402 -0
  69. package/dist/connectors/intercom.js.map +1 -0
  70. package/dist/connectors/jira.d.ts +98 -0
  71. package/dist/connectors/jira.js +379 -0
  72. package/dist/connectors/jira.js.map +1 -0
  73. package/dist/connectors/linear.d.ts +69 -19
  74. package/dist/connectors/linear.js +170 -129
  75. package/dist/connectors/linear.js.map +1 -1
  76. package/dist/connectors/mcpClient.d.ts +56 -0
  77. package/dist/connectors/mcpClient.js +189 -0
  78. package/dist/connectors/mcpClient.js.map +1 -0
  79. package/dist/connectors/mcpOAuth.d.ts +84 -0
  80. package/dist/connectors/mcpOAuth.js +389 -0
  81. package/dist/connectors/mcpOAuth.js.map +1 -0
  82. package/dist/connectors/mockConnector.d.ts +28 -0
  83. package/dist/connectors/mockConnector.js +81 -0
  84. package/dist/connectors/mockConnector.js.map +1 -0
  85. package/dist/connectors/notion.d.ts +143 -0
  86. package/dist/connectors/notion.js +424 -0
  87. package/dist/connectors/notion.js.map +1 -0
  88. package/dist/connectors/sentry.d.ts +17 -21
  89. package/dist/connectors/sentry.js +115 -131
  90. package/dist/connectors/sentry.js.map +1 -1
  91. package/dist/connectors/slack.d.ts +50 -0
  92. package/dist/connectors/slack.js +324 -0
  93. package/dist/connectors/slack.js.map +1 -0
  94. package/dist/connectors/stripe.d.ts +116 -0
  95. package/dist/connectors/stripe.js +379 -0
  96. package/dist/connectors/stripe.js.map +1 -0
  97. package/dist/connectors/tokenStorage.d.ts +35 -0
  98. package/dist/connectors/tokenStorage.js +459 -0
  99. package/dist/connectors/tokenStorage.js.map +1 -0
  100. package/dist/connectors/zendesk.d.ts +104 -0
  101. package/dist/connectors/zendesk.js +424 -0
  102. package/dist/connectors/zendesk.js.map +1 -0
  103. package/dist/drivers/gemini/index.d.ts +5 -1
  104. package/dist/drivers/gemini/index.js +39 -5
  105. package/dist/drivers/gemini/index.js.map +1 -1
  106. package/dist/drivers/index.d.ts +5 -0
  107. package/dist/drivers/index.js +1 -1
  108. package/dist/drivers/index.js.map +1 -1
  109. package/dist/featureFlags.d.ts +73 -0
  110. package/dist/featureFlags.js +203 -0
  111. package/dist/featureFlags.js.map +1 -0
  112. package/dist/fp/automationInterpreter.js +1 -0
  113. package/dist/fp/automationInterpreter.js.map +1 -1
  114. package/dist/fp/automationProgram.d.ts +1 -1
  115. package/dist/fp/automationProgram.js.map +1 -1
  116. package/dist/fp/policyParser.js +17 -0
  117. package/dist/fp/policyParser.js.map +1 -1
  118. package/dist/index.js +621 -61
  119. package/dist/index.js.map +1 -1
  120. package/dist/installGuard.d.ts +25 -0
  121. package/dist/installGuard.js +48 -0
  122. package/dist/installGuard.js.map +1 -0
  123. package/dist/oauth.d.ts +4 -1
  124. package/dist/oauth.js +50 -14
  125. package/dist/oauth.js.map +1 -1
  126. package/dist/patchworkConfig.d.ts +9 -0
  127. package/dist/patchworkConfig.js.map +1 -1
  128. package/dist/recipes/RecipeOrchestrator.d.ts +40 -0
  129. package/dist/recipes/RecipeOrchestrator.js +51 -0
  130. package/dist/recipes/RecipeOrchestrator.js.map +1 -0
  131. package/dist/recipes/agentExecutor.d.ts +28 -0
  132. package/dist/recipes/agentExecutor.js +42 -0
  133. package/dist/recipes/agentExecutor.js.map +1 -0
  134. package/dist/recipes/chainedRunner.d.ts +140 -0
  135. package/dist/recipes/chainedRunner.js +539 -0
  136. package/dist/recipes/chainedRunner.js.map +1 -0
  137. package/dist/recipes/dependencyGraph.d.ts +39 -0
  138. package/dist/recipes/dependencyGraph.js +199 -0
  139. package/dist/recipes/dependencyGraph.js.map +1 -0
  140. package/dist/recipes/legacyRecipeCompat.d.ts +2 -0
  141. package/dist/recipes/legacyRecipeCompat.js +112 -0
  142. package/dist/recipes/legacyRecipeCompat.js.map +1 -0
  143. package/dist/recipes/manifest.d.ts +47 -0
  144. package/dist/recipes/manifest.js +141 -0
  145. package/dist/recipes/manifest.js.map +1 -0
  146. package/dist/recipes/nestedRecipeStep.d.ts +58 -0
  147. package/dist/recipes/nestedRecipeStep.js +95 -0
  148. package/dist/recipes/nestedRecipeStep.js.map +1 -0
  149. package/dist/recipes/outputRegistry.d.ts +28 -0
  150. package/dist/recipes/outputRegistry.js +52 -0
  151. package/dist/recipes/outputRegistry.js.map +1 -0
  152. package/dist/recipes/scheduler.d.ts +23 -7
  153. package/dist/recipes/scheduler.js +131 -41
  154. package/dist/recipes/scheduler.js.map +1 -1
  155. package/dist/recipes/schema.d.ts +17 -2
  156. package/dist/recipes/schemaGenerator.d.ts +28 -0
  157. package/dist/recipes/schemaGenerator.js +565 -0
  158. package/dist/recipes/schemaGenerator.js.map +1 -0
  159. package/dist/recipes/templateEngine.d.ts +62 -0
  160. package/dist/recipes/templateEngine.js +182 -0
  161. package/dist/recipes/templateEngine.js.map +1 -0
  162. package/dist/recipes/toolRegistry.d.ts +181 -0
  163. package/dist/recipes/toolRegistry.js +300 -0
  164. package/dist/recipes/toolRegistry.js.map +1 -0
  165. package/dist/recipes/tools/calendar.d.ts +6 -0
  166. package/dist/recipes/tools/calendar.js +61 -0
  167. package/dist/recipes/tools/calendar.js.map +1 -0
  168. package/dist/recipes/tools/confluence.d.ts +6 -0
  169. package/dist/recipes/tools/confluence.js +254 -0
  170. package/dist/recipes/tools/confluence.js.map +1 -0
  171. package/dist/recipes/tools/datadog.d.ts +6 -0
  172. package/dist/recipes/tools/datadog.js +239 -0
  173. package/dist/recipes/tools/datadog.js.map +1 -0
  174. package/dist/recipes/tools/diagnostics.d.ts +6 -0
  175. package/dist/recipes/tools/diagnostics.js +36 -0
  176. package/dist/recipes/tools/diagnostics.js.map +1 -0
  177. package/dist/recipes/tools/file.d.ts +6 -0
  178. package/dist/recipes/tools/file.js +170 -0
  179. package/dist/recipes/tools/file.js.map +1 -0
  180. package/dist/recipes/tools/git.d.ts +6 -0
  181. package/dist/recipes/tools/git.js +63 -0
  182. package/dist/recipes/tools/git.js.map +1 -0
  183. package/dist/recipes/tools/github.d.ts +6 -0
  184. package/dist/recipes/tools/github.js +91 -0
  185. package/dist/recipes/tools/github.js.map +1 -0
  186. package/dist/recipes/tools/gmail.d.ts +6 -0
  187. package/dist/recipes/tools/gmail.js +210 -0
  188. package/dist/recipes/tools/gmail.js.map +1 -0
  189. package/dist/recipes/tools/hubspot.d.ts +6 -0
  190. package/dist/recipes/tools/hubspot.js +232 -0
  191. package/dist/recipes/tools/hubspot.js.map +1 -0
  192. package/dist/recipes/tools/index.d.ts +22 -0
  193. package/dist/recipes/tools/index.js +25 -0
  194. package/dist/recipes/tools/index.js.map +1 -0
  195. package/dist/recipes/tools/intercom.d.ts +6 -0
  196. package/dist/recipes/tools/intercom.js +226 -0
  197. package/dist/recipes/tools/intercom.js.map +1 -0
  198. package/dist/recipes/tools/linear.d.ts +6 -0
  199. package/dist/recipes/tools/linear.js +83 -0
  200. package/dist/recipes/tools/linear.js.map +1 -0
  201. package/dist/recipes/tools/notion.d.ts +6 -0
  202. package/dist/recipes/tools/notion.js +278 -0
  203. package/dist/recipes/tools/notion.js.map +1 -0
  204. package/dist/recipes/tools/slack.d.ts +6 -0
  205. package/dist/recipes/tools/slack.js +72 -0
  206. package/dist/recipes/tools/slack.js.map +1 -0
  207. package/dist/recipes/tools/stripe.d.ts +6 -0
  208. package/dist/recipes/tools/stripe.js +265 -0
  209. package/dist/recipes/tools/stripe.js.map +1 -0
  210. package/dist/recipes/tools/zendesk.d.ts +6 -0
  211. package/dist/recipes/tools/zendesk.js +245 -0
  212. package/dist/recipes/tools/zendesk.js.map +1 -0
  213. package/dist/recipes/validation.d.ts +13 -0
  214. package/dist/recipes/validation.js +433 -0
  215. package/dist/recipes/validation.js.map +1 -0
  216. package/dist/recipes/yamlRunner.d.ts +87 -0
  217. package/dist/recipes/yamlRunner.js +693 -409
  218. package/dist/recipes/yamlRunner.js.map +1 -1
  219. package/dist/recipesHttp.d.ts +34 -6
  220. package/dist/recipesHttp.js +285 -15
  221. package/dist/recipesHttp.js.map +1 -1
  222. package/dist/riskTier.js +1 -0
  223. package/dist/riskTier.js.map +1 -1
  224. package/dist/runLog.d.ts +23 -0
  225. package/dist/runLog.js +56 -1
  226. package/dist/runLog.js.map +1 -1
  227. package/dist/schemas/dry-run-plan.v1.json +139 -0
  228. package/dist/schemas/recipe.v1.json +684 -0
  229. package/dist/server.d.ts +32 -1
  230. package/dist/server.js +980 -97
  231. package/dist/server.js.map +1 -1
  232. package/dist/streamableHttp.js +2 -0
  233. package/dist/streamableHttp.js.map +1 -1
  234. package/dist/tools/addLinearComment.d.ts +55 -0
  235. package/dist/tools/addLinearComment.js +72 -0
  236. package/dist/tools/addLinearComment.js.map +1 -0
  237. package/dist/tools/bridgeDoctor.js +2 -2
  238. package/dist/tools/bridgeDoctor.js.map +1 -1
  239. package/dist/tools/createLinearIssue.d.ts +84 -0
  240. package/dist/tools/createLinearIssue.js +146 -0
  241. package/dist/tools/createLinearIssue.js.map +1 -0
  242. package/dist/tools/fetchCalendarEvents.d.ts +94 -0
  243. package/dist/tools/fetchCalendarEvents.js +97 -0
  244. package/dist/tools/fetchCalendarEvents.js.map +1 -0
  245. package/dist/tools/fetchGithubIssue.d.ts +80 -0
  246. package/dist/tools/fetchGithubIssue.js +84 -0
  247. package/dist/tools/fetchGithubIssue.js.map +1 -0
  248. package/dist/tools/fetchGithubPR.d.ts +89 -0
  249. package/dist/tools/fetchGithubPR.js +96 -0
  250. package/dist/tools/fetchGithubPR.js.map +1 -0
  251. package/dist/tools/fetchSlackProfile.d.ts +43 -0
  252. package/dist/tools/fetchSlackProfile.js +46 -0
  253. package/dist/tools/fetchSlackProfile.js.map +1 -0
  254. package/dist/tools/getConnectorStatus.d.ts +58 -0
  255. package/dist/tools/getConnectorStatus.js +56 -0
  256. package/dist/tools/getConnectorStatus.js.map +1 -0
  257. package/dist/tools/github/actions.js +4 -2
  258. package/dist/tools/github/actions.js.map +1 -1
  259. package/dist/tools/github/composite.d.ts +339 -0
  260. package/dist/tools/github/composite.js +343 -0
  261. package/dist/tools/github/composite.js.map +1 -0
  262. package/dist/tools/github/index.d.ts +2 -1
  263. package/dist/tools/github/index.js +2 -1
  264. package/dist/tools/github/index.js.map +1 -1
  265. package/dist/tools/github/issues.js +8 -4
  266. package/dist/tools/github/issues.js.map +1 -1
  267. package/dist/tools/github/pr.d.ts +122 -0
  268. package/dist/tools/github/pr.js +195 -5
  269. package/dist/tools/github/pr.js.map +1 -1
  270. package/dist/tools/index.js +32 -1
  271. package/dist/tools/index.js.map +1 -1
  272. package/dist/tools/searchTools.js +1 -1
  273. package/dist/tools/searchTools.js.map +1 -1
  274. package/dist/tools/slackListChannels.d.ts +65 -0
  275. package/dist/tools/slackListChannels.js +70 -0
  276. package/dist/tools/slackListChannels.js.map +1 -0
  277. package/dist/tools/slackPostMessage.d.ts +57 -0
  278. package/dist/tools/slackPostMessage.js +77 -0
  279. package/dist/tools/slackPostMessage.js.map +1 -0
  280. package/dist/tools/testTraceToSource.js +2 -2
  281. package/dist/tools/testTraceToSource.js.map +1 -1
  282. package/dist/tools/updateLinearIssue.d.ts +89 -0
  283. package/dist/tools/updateLinearIssue.js +117 -0
  284. package/dist/tools/updateLinearIssue.js.map +1 -0
  285. package/dist/transport.d.ts +7 -1
  286. package/dist/transport.js +85 -11
  287. package/dist/transport.js.map +1 -1
  288. package/package.json +5 -2
  289. package/scripts/start-all.sh +56 -19
  290. package/templates/automation-policies/recipe-authoring.json +25 -0
  291. package/templates/automation-policy.example.json +6 -0
  292. package/templates/co.patchwork-os.bridge.plist +34 -0
  293. package/templates/recipes/ctx-loop-test.yaml +75 -0
  294. package/templates/recipes/lint-on-save.yaml +1 -2
  295. package/templates/recipes/morning-brief-slack.yaml +57 -0
  296. package/templates/recipes/morning-brief.yaml +14 -6
  297. package/templates/recipes/project-health-check.yaml +50 -0
  298. package/templates/recipes/sentry-to-linear.yaml +77 -0
package/dist/bridge.js CHANGED
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { WebSocket } from "ws";
6
+ import { recordRecipeRun } from "./activationMetrics.js";
6
7
  import { ActivityLog } from "./activityLog.js";
7
8
  import { buildSummary } from "./analyticsAggregator.js";
8
9
  import { getAnalyticsPref } from "./analyticsPrefs.js";
@@ -11,21 +12,23 @@ import { getApprovalQueue } from "./approvalQueue.js";
11
12
  import { AutomationHooks, loadPolicy } from "./automation.js";
12
13
  import { loadOrCreateBridgeToken } from "./bridgeToken.js";
13
14
  import { repairBridgeToolsRulesIfStale } from "./bridgeToolsRules.js";
14
- import { createDriver } from "./claudeDriver.js";
15
15
  import { ClaudeOrchestrator } from "./claudeOrchestrator.js";
16
16
  import { CommitIssueLinkLog } from "./commitIssueLinkLog.js";
17
17
  import { DecisionTraceLog } from "./decisionTraceLog.js";
18
+ import { createDriver } from "./drivers/index.js";
18
19
  import { ExtensionClient } from "./extensionClient.js";
19
20
  import { FileLock } from "./fileLock.js";
20
21
  import { buildEnforcementReminder } from "./instructionsUtils.js";
21
22
  import { LockFileManager } from "./lockfile.js";
22
23
  import { Logger } from "./logger.js";
23
24
  import { OAuthServerImpl } from "./oauth.js";
25
+ import { loadConfig as loadPatchworkConfig, saveConfig as savePatchworkConfig, } from "./patchworkConfig.js";
24
26
  import { loadPlugins, loadPluginsFull } from "./pluginLoader.js";
25
27
  import { PluginWatcher } from "./pluginWatcher.js";
26
28
  import { probeAll } from "./probe.js";
29
+ import { RecipeOrchestrator } from "./recipes/RecipeOrchestrator.js";
27
30
  import { RecipeScheduler } from "./recipes/scheduler.js";
28
- import { findWebhookRecipe, listInstalledRecipes, loadRecipePrompt, renderWebhookPrompt, saveRecipe, } from "./recipesHttp.js";
31
+ import { findWebhookRecipe, findYamlRecipePath, listInstalledRecipes, loadRecipeContent, loadRecipePrompt, renderWebhookPrompt, saveRecipe, saveRecipeContent, } from "./recipesHttp.js";
29
32
  import { classifyTool } from "./riskTier.js";
30
33
  import { RecipeRunLog } from "./runLog.js";
31
34
  import { Server } from "./server.js";
@@ -114,6 +117,7 @@ export class Bridge {
114
117
  automationHooks = undefined;
115
118
  recipeScheduler = null;
116
119
  recipeRunLog = null;
120
+ recipeOrchestrator = null;
117
121
  commitIssueLinkLog = null;
118
122
  decisionTraceLog = null;
119
123
  /** Pre-computed digest of recent decisions, refreshed on each session connect. */
@@ -136,6 +140,7 @@ export class Bridge {
136
140
  const configDir = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
137
141
  this.authToken = config.fixedToken ?? loadOrCreateBridgeToken(configDir);
138
142
  this.server = new Server(this.authToken, this.logger, config.corsOrigins);
143
+ this.server.bridgeConfigPath = config.configFilePath ?? undefined;
139
144
  if (config.issuerUrl) {
140
145
  this.oauthServer = new OAuthServerImpl(this.authToken, config.issuerUrl, {
141
146
  configDir,
@@ -239,6 +244,8 @@ export class Bridge {
239
244
  transport.sessionId = sessionId;
240
245
  transport.setActivityLog(this.activityLog);
241
246
  transport.setToolRateLimit(this.config.toolRateLimit);
247
+ if (this.config.lazyTools)
248
+ transport.setLazyTools(true);
242
249
  if (this.server.approvalGate !== "off") {
243
250
  this.logger.info(`[patchwork] approval gate active: ${this.server.approvalGate} tier(s) require dashboard approval`);
244
251
  transport.setApprovalGate(async ({ toolName, params, sessionId }) => {
@@ -309,6 +316,7 @@ export class Bridge {
309
316
  graceTimer: null,
310
317
  connectedAt: Date.now(),
311
318
  wsAlive: true,
319
+ remoteAddr: ws.remoteAddr,
312
320
  };
313
321
  ws.on("pong", () => {
314
322
  session.wsAlive = true;
@@ -702,8 +710,19 @@ export class Bridge {
702
710
  this.logger.info(`Available linters: ${probeList(["tsc", "eslint", "pyright", "ruff", "cargo", "go", "biome"])}`);
703
711
  this.logger.info(`Available test runners: ${probeList(["vitest", "jest", "pytest", "cargo", "go"])}`);
704
712
  // 2. Initialize Claude driver and orchestrator (if configured)
705
- if (this.config.claudeDriver !== "none") {
706
- const driver = createDriver(this.config.claudeDriver, this.config.claudeBinary, this.config.antBinary, (msg) => this.logger.info(msg));
713
+ if (this.config.driver !== "none") {
714
+ const driver = createDriver(this.config.driver, {
715
+ binary: this.config.claudeBinary,
716
+ antBinary: this.config.antBinary,
717
+ bridgeMcp: this.config.driver === "gemini"
718
+ ? () => this.port > 0
719
+ ? {
720
+ url: `http://127.0.0.1:${this.port}/mcp`,
721
+ authToken: this.authToken,
722
+ }
723
+ : undefined
724
+ : undefined,
725
+ }, (msg) => this.logger.info(msg));
707
726
  // Patchwork: enrichment link log is useful regardless of orchestrator.
708
727
  const patchworkDir = path.join(os.homedir(), ".patchwork");
709
728
  this.commitIssueLinkLog = new CommitIssueLinkLog({
@@ -763,17 +782,25 @@ export class Bridge {
763
782
  },
764
783
  });
765
784
  this.logger.info(`[bridge] Claude driver: ${driver.name}`);
785
+ // Recipe orchestrator — owns in-flight dedup across all entry paths.
786
+ this.recipeOrchestrator = new RecipeOrchestrator({
787
+ workdir: this.config.workspace,
788
+ });
766
789
  // Patchwork: start cron-trigger scheduler once the orchestrator exists.
767
790
  const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
768
791
  this.recipeScheduler = new RecipeScheduler({
769
792
  recipesDir,
770
- enqueue: (opts) => this.orchestrator.enqueue(opts),
793
+ enqueue: (opts) => this.orchestrator?.enqueue(opts) ?? "",
794
+ runYaml: async (name) => {
795
+ const result = await this.server.runRecipeFn?.(name);
796
+ if (result && !result.ok) {
797
+ throw new Error(result.error ?? "unknown error");
798
+ }
799
+ },
771
800
  logger: this.logger,
772
801
  });
773
- const scheduled = this.recipeScheduler.start();
774
- if (scheduled.length > 0) {
775
- this.logger.info(`[patchwork] scheduled ${scheduled.length} cron recipe${scheduled.length === 1 ? "" : "s"}`);
776
- }
802
+ // scheduler.start() deferred to after this.port is set (see below)
803
+ // so bridgeMcp callback has a valid port when first cron fires.
777
804
  }
778
805
  }
779
806
  if (this.config.automationEnabled) {
@@ -973,6 +1000,7 @@ export class Bridge {
973
1000
  };
974
1001
  };
975
1002
  this.server.streamFn = (listener) => this.activityLog.subscribe(listener);
1003
+ this.server.cancelTaskFn = (id) => this.orchestrator?.cancel(id, "user") ?? false;
976
1004
  this.server.tasksFn = () => ({
977
1005
  tasks: (this.orchestrator?.list() ?? []).map((t) => ({
978
1006
  taskId: t.id,
@@ -1003,10 +1031,43 @@ export class Bridge {
1003
1031
  const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
1004
1032
  return listInstalledRecipes(recipesDir);
1005
1033
  };
1034
+ this.server.loadRecipeContentFn = (name) => {
1035
+ const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
1036
+ return loadRecipeContent(recipesDir, name);
1037
+ };
1038
+ this.server.saveRecipeContentFn = (name, content) => {
1039
+ const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
1040
+ return saveRecipeContent(recipesDir, name, content);
1041
+ };
1006
1042
  this.server.saveRecipeFn = (draft) => {
1007
1043
  const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
1008
1044
  return saveRecipe(recipesDir, draft);
1009
1045
  };
1046
+ this.server.setRecipeEnabledFn = (name, enabled) => {
1047
+ try {
1048
+ const cfg = loadPatchworkConfig();
1049
+ const disabled = new Set(cfg.recipes?.disabled ??
1050
+ []);
1051
+ if (enabled)
1052
+ disabled.delete(name);
1053
+ else
1054
+ disabled.add(name);
1055
+ savePatchworkConfig({
1056
+ ...cfg,
1057
+ recipes: {
1058
+ ...cfg.recipes,
1059
+ disabled: [...disabled],
1060
+ },
1061
+ });
1062
+ return { ok: true };
1063
+ }
1064
+ catch (err) {
1065
+ return {
1066
+ ok: false,
1067
+ error: err instanceof Error ? err.message : String(err),
1068
+ };
1069
+ }
1070
+ };
1010
1071
  this.server.runsFn = (q) => {
1011
1072
  if (!this.recipeRunLog)
1012
1073
  return [];
@@ -1022,14 +1083,28 @@ export class Bridge {
1022
1083
  ...(q.after !== undefined && { after: q.after }),
1023
1084
  });
1024
1085
  };
1025
- this.server.sessionsFn = () => [...this.sessions.values()].map((s) => ({
1026
- id: s.id,
1027
- connectedAt: new Date(s.connectedAt).toISOString(),
1028
- openedFileCount: s.openedFiles.size,
1029
- pendingApprovals: getApprovalQueue()
1030
- .list()
1031
- .filter((a) => a.sessionId === s.id).length,
1032
- }));
1086
+ this.server.runDetailFn = (seq) => {
1087
+ if (!this.recipeRunLog)
1088
+ return null;
1089
+ return this.recipeRunLog.getBySeq(seq);
1090
+ };
1091
+ this.server.runPlanFn = async (recipeName) => {
1092
+ const { runRecipeDryPlan } = await import("./commands/recipe.js");
1093
+ return (await runRecipeDryPlan(recipeName));
1094
+ };
1095
+ this.server.sessionsFn = () => [...this.sessions.values()].map((s) => {
1096
+ const tools = this.activityLog.querySessionTools(s.id, 1);
1097
+ return {
1098
+ id: s.id,
1099
+ connectedAt: new Date(s.connectedAt).toISOString(),
1100
+ openedFileCount: s.openedFiles.size,
1101
+ pendingApprovals: getApprovalQueue()
1102
+ .list()
1103
+ .filter((a) => a.sessionId === s.id).length,
1104
+ firstTool: tools[0]?.tool,
1105
+ remoteAddr: s.remoteAddr,
1106
+ };
1107
+ });
1033
1108
  this.server.sessionDetailFn = (id) => {
1034
1109
  const s = this.sessions.get(id);
1035
1110
  const summary = s
@@ -1068,7 +1143,36 @@ export class Bridge {
1068
1143
  if (!match) {
1069
1144
  return { ok: false, error: "not_found" };
1070
1145
  }
1071
- const loaded = loadRecipePrompt(recipesDir, match.name);
1146
+ if (match.format === "yaml") {
1147
+ let payloadText;
1148
+ if (payload !== undefined) {
1149
+ try {
1150
+ payloadText = JSON.stringify(payload);
1151
+ }
1152
+ catch {
1153
+ payloadText = String(payload);
1154
+ }
1155
+ if (payloadText.length > 8_000) {
1156
+ payloadText = `${payloadText.slice(0, 8_000)}\n…[truncated]`;
1157
+ }
1158
+ }
1159
+ const seedContext = {
1160
+ hook_path: hookPath,
1161
+ webhook_path: hookPath,
1162
+ ...(payloadText !== undefined
1163
+ ? { payload: payloadText, webhook_payload: payloadText }
1164
+ : {}),
1165
+ };
1166
+ return this._fireYamlRecipe({
1167
+ filePath: match.filePath,
1168
+ name: match.name,
1169
+ taskIdPrefix: `yaml-webhook-${match.name}`,
1170
+ triggerSourceSuffix: `webhook:${match.name}`,
1171
+ logLabel: `webhook "${match.name}"`,
1172
+ seedContext,
1173
+ });
1174
+ }
1175
+ const loaded = loadRecipePrompt(recipesDir, path.basename(match.filePath, path.extname(match.filePath)));
1072
1176
  if (!loaded) {
1073
1177
  return { ok: false, error: "recipe_file_missing" };
1074
1178
  }
@@ -1086,7 +1190,7 @@ export class Bridge {
1086
1190
  };
1087
1191
  }
1088
1192
  };
1089
- this.server.runRecipeFn = async (name) => {
1193
+ this.server.runRecipeFn = async (name, vars) => {
1090
1194
  if (!this.orchestrator) {
1091
1195
  return {
1092
1196
  ok: false,
@@ -1094,26 +1198,45 @@ export class Bridge {
1094
1198
  };
1095
1199
  }
1096
1200
  const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
1201
+ // Try JSON recipe first (legacy path: enqueue prompt as a task).
1097
1202
  const loaded = loadRecipePrompt(recipesDir, name);
1098
- if (!loaded) {
1099
- return {
1100
- ok: false,
1101
- error: `Recipe "${name}" not found in ${recipesDir}`,
1102
- };
1103
- }
1104
- try {
1105
- const taskId = this.orchestrator.enqueue({
1106
- prompt: loaded.prompt,
1107
- triggerSource: `recipe:${name}`,
1108
- });
1109
- return { ok: true, taskId };
1203
+ if (loaded) {
1204
+ try {
1205
+ let prompt = loaded.prompt;
1206
+ if (vars && Object.keys(vars).length > 0) {
1207
+ const varLines = Object.entries(vars)
1208
+ .map(([k, v]) => `${k}=${v}`)
1209
+ .join("\n");
1210
+ prompt = `Variables:\n${varLines}\n\n${prompt}`;
1211
+ }
1212
+ const taskId = this.orchestrator.enqueue({
1213
+ prompt,
1214
+ triggerSource: `recipe:${name}`,
1215
+ });
1216
+ return { ok: true, taskId };
1217
+ }
1218
+ catch (err) {
1219
+ return {
1220
+ ok: false,
1221
+ error: err instanceof Error ? err.message : String(err),
1222
+ };
1223
+ }
1110
1224
  }
1111
- catch (err) {
1225
+ // Fall through to YAML runner for .yaml/.yml recipes.
1226
+ const ymlPath = findYamlRecipePath(recipesDir, name);
1227
+ if (!ymlPath) {
1112
1228
  return {
1113
1229
  ok: false,
1114
- error: err instanceof Error ? err.message : String(err),
1230
+ error: `Recipe "${name}" not found in ${recipesDir}`,
1115
1231
  };
1116
1232
  }
1233
+ return this._fireYamlRecipe({
1234
+ filePath: ymlPath,
1235
+ name,
1236
+ taskIdPrefix: `yaml-recipe-${name}`,
1237
+ triggerSourceSuffix: `recipe:${name}`,
1238
+ logLabel: `"${name}"`,
1239
+ });
1117
1240
  };
1118
1241
  this.server.readyFn = () => {
1119
1242
  // Count tools from the first active session (all sessions share the same tool set)
@@ -1156,7 +1279,10 @@ export class Bridge {
1156
1279
  workspace: this.config.workspace,
1157
1280
  approvalGate: this.server.approvalGate,
1158
1281
  fullMode: this.config.fullMode,
1159
- claudeDriver: this.config.claudeDriver,
1282
+ driver: this.config.driver,
1283
+ model: loadPatchworkConfig().model,
1284
+ localEndpoint: loadPatchworkConfig().localEndpoint,
1285
+ localModel: loadPatchworkConfig().localModel,
1160
1286
  automationEnabled: this.config.automationEnabled,
1161
1287
  port: this.port,
1162
1288
  webhookUrl: this.server.approvalWebhookUrl ?? null,
@@ -1262,6 +1388,13 @@ export class Bridge {
1262
1388
  throw err;
1263
1389
  }
1264
1390
  this.port = port;
1391
+ // 4a-deferred. Start recipe scheduler now that port is known (bridgeMcp needs a valid port).
1392
+ if (this.recipeScheduler) {
1393
+ const scheduled = this.recipeScheduler.start();
1394
+ if (scheduled.length > 0) {
1395
+ this.logger.info(`[patchwork] scheduled ${scheduled.length} cron recipe${scheduled.length === 1 ? "" : "s"}`);
1396
+ }
1397
+ }
1265
1398
  // 4b. Start WebSocket keepalive heartbeat (keeps MCP session alive during long idle periods)
1266
1399
  this._startWsHeartbeat();
1267
1400
  // 4c. Load persisted tasks from previous sessions (best-effort)
@@ -1392,6 +1525,63 @@ export class Bridge {
1392
1525
  })}`);
1393
1526
  }
1394
1527
  }
1528
+ /**
1529
+ * Load and fire a YAML recipe in the background via the orchestrator.
1530
+ * Returns `{ ok, taskId, name? }` immediately; execution continues async.
1531
+ * Both the webhook path and runRecipeFn use this to eliminate duplication.
1532
+ */
1533
+ async _fireYamlRecipe(opts) {
1534
+ if (!this.recipeOrchestrator) {
1535
+ return { ok: false, error: "recipe orchestrator unavailable" };
1536
+ }
1537
+ const orch = this.orchestrator;
1538
+ const { buildChainedDeps, dispatchRecipe } = await import("./recipes/yamlRunner.js");
1539
+ const claudeCodeFn = async (prompt) => {
1540
+ const task = await orch.runAndWait({
1541
+ prompt,
1542
+ triggerSource: `${opts.triggerSourceSuffix}:agent`,
1543
+ timeoutMs: 600_000,
1544
+ });
1545
+ return task.output ?? task.errorMessage ?? "";
1546
+ };
1547
+ const runnerDeps = { workdir: this.config.workspace, claudeCodeFn };
1548
+ const chainedOptions = {
1549
+ sourcePath: opts.filePath,
1550
+ runLogDir: this.recipeRunLog
1551
+ ? path.join(os.homedir(), ".patchwork")
1552
+ : undefined,
1553
+ };
1554
+ const fireResult = await this.recipeOrchestrator
1555
+ .fire({
1556
+ filePath: opts.filePath,
1557
+ name: opts.name,
1558
+ triggerSource: opts.triggerSourceSuffix,
1559
+ seedContext: opts.seedContext,
1560
+ dispatchFn: async (recipe, _deps, seedContext) => {
1561
+ const result = await dispatchRecipe(recipe, {
1562
+ ...runnerDeps,
1563
+ chainedDeps: buildChainedDeps(runnerDeps, claudeCodeFn),
1564
+ chainedOptions,
1565
+ }, seedContext);
1566
+ const steps = "stepsRun" in result
1567
+ ? result.stepsRun
1568
+ : (result.summary?.total ?? "?");
1569
+ const succeeded = "stepsRun" in result ? !result.errorMessage : result.success;
1570
+ if (succeeded)
1571
+ recordRecipeRun();
1572
+ this.logger.info?.(`[recipe] ${opts.logLabel} finished: ${steps} steps`);
1573
+ return result;
1574
+ },
1575
+ })
1576
+ .catch((err) => {
1577
+ this.logger.warn?.(`[recipe] ${opts.logLabel} error: ${err instanceof Error ? err.message : String(err)}`);
1578
+ return {
1579
+ ok: false,
1580
+ error: err instanceof Error ? err.message : String(err),
1581
+ };
1582
+ });
1583
+ return fireResult;
1584
+ }
1395
1585
  /** Start the bridge-level WebSocket keepalive heartbeat. Idempotent. */
1396
1586
  _startWsHeartbeat() {
1397
1587
  if (this.wsHeartbeatInterval || this.config.wsPingIntervalMs === 0)
@@ -1458,7 +1648,7 @@ export class Bridge {
1458
1648
  if (this.checkpoint && this.port > 0) {
1459
1649
  try {
1460
1650
  await Promise.race([
1461
- Promise.resolve().then(() => this.checkpoint.write(this._buildCheckpoint(this.port))),
1651
+ Promise.resolve().then(() => this.checkpoint?.write(this._buildCheckpoint(this.port))),
1462
1652
  new Promise((resolve) => setTimeout(resolve, 3000)),
1463
1653
  ]);
1464
1654
  }