patchwork-os 0.2.0-alpha.9 → 0.2.0-beta.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 (581) hide show
  1. package/README.bridge.md +6 -0
  2. package/README.md +315 -35
  3. package/deploy/bootstrap-new-vps.sh +12 -12
  4. package/deploy/bootstrap-vps.sh +187 -0
  5. package/deploy/deploy-dashboard.sh +174 -0
  6. package/deploy/deploy-landing.sh +136 -0
  7. package/dist/activationMetrics.d.ts +67 -0
  8. package/dist/activationMetrics.js +255 -0
  9. package/dist/activationMetrics.js.map +1 -0
  10. package/dist/activityLog.d.ts +49 -0
  11. package/dist/activityLog.js +78 -0
  12. package/dist/activityLog.js.map +1 -1
  13. package/dist/approvalHttp.d.ts +49 -2
  14. package/dist/approvalHttp.js +217 -21
  15. package/dist/approvalHttp.js.map +1 -1
  16. package/dist/approvalInsights.d.ts +49 -0
  17. package/dist/approvalInsights.js +97 -0
  18. package/dist/approvalInsights.js.map +1 -0
  19. package/dist/approvalQueue.d.ts +27 -1
  20. package/dist/approvalQueue.js +123 -3
  21. package/dist/approvalQueue.js.map +1 -1
  22. package/dist/approvalSignals.d.ts +124 -0
  23. package/dist/approvalSignals.js +512 -0
  24. package/dist/approvalSignals.js.map +1 -0
  25. package/dist/automation.d.ts +57 -0
  26. package/dist/automation.js +156 -59
  27. package/dist/automation.js.map +1 -1
  28. package/dist/automationSuggestions.d.ts +79 -0
  29. package/dist/automationSuggestions.js +150 -0
  30. package/dist/automationSuggestions.js.map +1 -0
  31. package/dist/bridge.d.ts +3 -0
  32. package/dist/bridge.js +174 -143
  33. package/dist/bridge.js.map +1 -1
  34. package/dist/bridgeToken.js +57 -19
  35. package/dist/bridgeToken.js.map +1 -1
  36. package/dist/ccPermissions.d.ts +15 -0
  37. package/dist/ccPermissions.js +21 -4
  38. package/dist/ccPermissions.js.map +1 -1
  39. package/dist/claudeDriver.js +74 -16
  40. package/dist/claudeDriver.js.map +1 -1
  41. package/dist/claudeOrchestrator.d.ts +1 -1
  42. package/dist/claudeOrchestrator.js +14 -8
  43. package/dist/claudeOrchestrator.js.map +1 -1
  44. package/dist/commands/dashboard.js +1 -1
  45. package/dist/commands/dashboard.js.map +1 -1
  46. package/dist/commands/launchd.d.ts +2 -0
  47. package/dist/commands/launchd.js +94 -0
  48. package/dist/commands/launchd.js.map +1 -0
  49. package/dist/commands/patchworkInit.d.ts +8 -0
  50. package/dist/commands/patchworkInit.js +77 -11
  51. package/dist/commands/patchworkInit.js.map +1 -1
  52. package/dist/commands/recipe.d.ts +289 -0
  53. package/dist/commands/recipe.js +1359 -0
  54. package/dist/commands/recipe.js.map +1 -0
  55. package/dist/commands/recipeInstall.d.ts +150 -0
  56. package/dist/commands/recipeInstall.js +647 -0
  57. package/dist/commands/recipeInstall.js.map +1 -0
  58. package/dist/commands/tracesExport.d.ts +83 -0
  59. package/dist/commands/tracesExport.js +269 -0
  60. package/dist/commands/tracesExport.js.map +1 -0
  61. package/dist/commands/tracesImport.d.ts +56 -0
  62. package/dist/commands/tracesImport.js +161 -0
  63. package/dist/commands/tracesImport.js.map +1 -0
  64. package/dist/config.d.ts +22 -1
  65. package/dist/config.js +108 -9
  66. package/dist/config.js.map +1 -1
  67. package/dist/connectorRoutes.d.ts +43 -0
  68. package/dist/connectorRoutes.js +1609 -0
  69. package/dist/connectorRoutes.js.map +1 -0
  70. package/dist/connectors/asana.d.ts +198 -0
  71. package/dist/connectors/asana.js +679 -0
  72. package/dist/connectors/asana.js.map +1 -0
  73. package/dist/connectors/baseConnector.d.ts +153 -0
  74. package/dist/connectors/baseConnector.js +336 -0
  75. package/dist/connectors/baseConnector.js.map +1 -0
  76. package/dist/connectors/confluence.d.ts +111 -0
  77. package/dist/connectors/confluence.js +406 -0
  78. package/dist/connectors/confluence.js.map +1 -0
  79. package/dist/connectors/datadog.d.ts +116 -0
  80. package/dist/connectors/datadog.js +385 -0
  81. package/dist/connectors/datadog.js.map +1 -0
  82. package/dist/connectors/discord.d.ts +150 -0
  83. package/dist/connectors/discord.js +543 -0
  84. package/dist/connectors/discord.js.map +1 -0
  85. package/dist/connectors/fixtureLibrary.d.ts +21 -0
  86. package/dist/connectors/fixtureLibrary.js +70 -0
  87. package/dist/connectors/fixtureLibrary.js.map +1 -0
  88. package/dist/connectors/fixtureRecorder.d.ts +1 -0
  89. package/dist/connectors/fixtureRecorder.js +35 -0
  90. package/dist/connectors/fixtureRecorder.js.map +1 -0
  91. package/dist/connectors/github.js +17 -18
  92. package/dist/connectors/github.js.map +1 -1
  93. package/dist/connectors/gitlab.d.ts +180 -0
  94. package/dist/connectors/gitlab.js +582 -0
  95. package/dist/connectors/gitlab.js.map +1 -0
  96. package/dist/connectors/gmail.d.ts +4 -1
  97. package/dist/connectors/gmail.js +149 -27
  98. package/dist/connectors/gmail.js.map +1 -1
  99. package/dist/connectors/googleCalendar.d.ts +4 -1
  100. package/dist/connectors/googleCalendar.js +88 -25
  101. package/dist/connectors/googleCalendar.js.map +1 -1
  102. package/dist/connectors/googleDrive.d.ts +34 -0
  103. package/dist/connectors/googleDrive.js +321 -0
  104. package/dist/connectors/googleDrive.js.map +1 -0
  105. package/dist/connectors/htmlEscape.d.ts +5 -0
  106. package/dist/connectors/htmlEscape.js +13 -0
  107. package/dist/connectors/htmlEscape.js.map +1 -0
  108. package/dist/connectors/hubspot.d.ts +112 -0
  109. package/dist/connectors/hubspot.js +408 -0
  110. package/dist/connectors/hubspot.js.map +1 -0
  111. package/dist/connectors/intercom.d.ts +102 -0
  112. package/dist/connectors/intercom.js +402 -0
  113. package/dist/connectors/intercom.js.map +1 -0
  114. package/dist/connectors/jira.d.ts +98 -0
  115. package/dist/connectors/jira.js +379 -0
  116. package/dist/connectors/jira.js.map +1 -0
  117. package/dist/connectors/linear.js +30 -19
  118. package/dist/connectors/linear.js.map +1 -1
  119. package/dist/connectors/mcpOAuth.d.ts +3 -0
  120. package/dist/connectors/mcpOAuth.js +64 -10
  121. package/dist/connectors/mcpOAuth.js.map +1 -1
  122. package/dist/connectors/mockConnector.d.ts +28 -0
  123. package/dist/connectors/mockConnector.js +81 -0
  124. package/dist/connectors/mockConnector.js.map +1 -0
  125. package/dist/connectors/notion.d.ts +143 -0
  126. package/dist/connectors/notion.js +424 -0
  127. package/dist/connectors/notion.js.map +1 -0
  128. package/dist/connectors/oauthStateStore.d.ts +31 -0
  129. package/dist/connectors/oauthStateStore.js +52 -0
  130. package/dist/connectors/oauthStateStore.js.map +1 -0
  131. package/dist/connectors/pagerduty.d.ts +160 -0
  132. package/dist/connectors/pagerduty.js +464 -0
  133. package/dist/connectors/pagerduty.js.map +1 -0
  134. package/dist/connectors/sentry.js +5 -13
  135. package/dist/connectors/sentry.js.map +1 -1
  136. package/dist/connectors/slack.d.ts +16 -1
  137. package/dist/connectors/slack.js +155 -32
  138. package/dist/connectors/slack.js.map +1 -1
  139. package/dist/connectors/stripe.d.ts +116 -0
  140. package/dist/connectors/stripe.js +379 -0
  141. package/dist/connectors/stripe.js.map +1 -0
  142. package/dist/connectors/tokenStorage.d.ts +35 -0
  143. package/dist/connectors/tokenStorage.js +484 -0
  144. package/dist/connectors/tokenStorage.js.map +1 -0
  145. package/dist/connectors/zendesk.d.ts +104 -0
  146. package/dist/connectors/zendesk.js +442 -0
  147. package/dist/connectors/zendesk.js.map +1 -0
  148. package/dist/cors.d.ts +10 -0
  149. package/dist/cors.js +29 -0
  150. package/dist/cors.js.map +1 -0
  151. package/dist/decisionReplay.d.ts +72 -0
  152. package/dist/decisionReplay.js +92 -0
  153. package/dist/decisionReplay.js.map +1 -0
  154. package/dist/decisionTraceLog.d.ts +6 -0
  155. package/dist/decisionTraceLog.js +54 -2
  156. package/dist/decisionTraceLog.js.map +1 -1
  157. package/dist/drivers/gemini/index.d.ts +5 -1
  158. package/dist/drivers/gemini/index.js +39 -5
  159. package/dist/drivers/gemini/index.js.map +1 -1
  160. package/dist/drivers/index.d.ts +5 -0
  161. package/dist/drivers/index.js +1 -1
  162. package/dist/drivers/index.js.map +1 -1
  163. package/dist/featureFlags.d.ts +79 -0
  164. package/dist/featureFlags.js +208 -0
  165. package/dist/featureFlags.js.map +1 -0
  166. package/dist/fp/automationInterpreter.js +26 -21
  167. package/dist/fp/automationInterpreter.js.map +1 -1
  168. package/dist/fp/automationProgram.d.ts +1 -1
  169. package/dist/fp/automationProgram.js.map +1 -1
  170. package/dist/fp/automationState.js +4 -1
  171. package/dist/fp/automationState.js.map +1 -1
  172. package/dist/fp/policyParser.js +21 -1
  173. package/dist/fp/policyParser.js.map +1 -1
  174. package/dist/inboxRoutes.d.ts +22 -0
  175. package/dist/inboxRoutes.js +114 -0
  176. package/dist/inboxRoutes.js.map +1 -0
  177. package/dist/index.js +1400 -201
  178. package/dist/index.js.map +1 -1
  179. package/dist/installGuard.d.ts +25 -0
  180. package/dist/installGuard.js +48 -0
  181. package/dist/installGuard.js.map +1 -0
  182. package/dist/mcpRoutes.d.ts +37 -0
  183. package/dist/mcpRoutes.js +76 -0
  184. package/dist/mcpRoutes.js.map +1 -0
  185. package/dist/oauth.d.ts +7 -1
  186. package/dist/oauth.js +201 -39
  187. package/dist/oauth.js.map +1 -1
  188. package/dist/oauthRoutes.d.ts +32 -0
  189. package/dist/oauthRoutes.js +124 -0
  190. package/dist/oauthRoutes.js.map +1 -0
  191. package/dist/orchestrator/orchestratorBridge.js +2 -2
  192. package/dist/orchestrator/orchestratorBridge.js.map +1 -1
  193. package/dist/patchworkConfig.d.ts +16 -0
  194. package/dist/patchworkConfig.js +1 -1
  195. package/dist/patchworkConfig.js.map +1 -1
  196. package/dist/pluginLoader.d.ts +28 -0
  197. package/dist/pluginLoader.js +77 -11
  198. package/dist/pluginLoader.js.map +1 -1
  199. package/dist/pluginWatcher.js +8 -3
  200. package/dist/pluginWatcher.js.map +1 -1
  201. package/dist/preToolUseHook.d.ts +12 -0
  202. package/dist/preToolUseHook.js +23 -0
  203. package/dist/preToolUseHook.js.map +1 -1
  204. package/dist/recipeOrchestration.d.ts +121 -0
  205. package/dist/recipeOrchestration.js +955 -0
  206. package/dist/recipeOrchestration.js.map +1 -0
  207. package/dist/recipeRoutes.d.ts +180 -0
  208. package/dist/recipeRoutes.js +1345 -0
  209. package/dist/recipeRoutes.js.map +1 -0
  210. package/dist/recipes/RecipeOrchestrator.d.ts +40 -0
  211. package/dist/recipes/RecipeOrchestrator.js +51 -0
  212. package/dist/recipes/RecipeOrchestrator.js.map +1 -0
  213. package/dist/recipes/agentExecutor.d.ts +29 -0
  214. package/dist/recipes/agentExecutor.js +49 -0
  215. package/dist/recipes/agentExecutor.js.map +1 -0
  216. package/dist/recipes/chainedRunner.d.ts +191 -0
  217. package/dist/recipes/chainedRunner.js +759 -0
  218. package/dist/recipes/chainedRunner.js.map +1 -0
  219. package/dist/recipes/compiler.js +3 -3
  220. package/dist/recipes/compiler.js.map +1 -1
  221. package/dist/recipes/dependencyGraph.d.ts +39 -0
  222. package/dist/recipes/dependencyGraph.js +199 -0
  223. package/dist/recipes/dependencyGraph.js.map +1 -0
  224. package/dist/recipes/disabledMarkers.d.ts +48 -0
  225. package/dist/recipes/disabledMarkers.js +52 -0
  226. package/dist/recipes/disabledMarkers.js.map +1 -0
  227. package/dist/recipes/installer.js +3 -3
  228. package/dist/recipes/installer.js.map +1 -1
  229. package/dist/recipes/legacyRecipeCompat.d.ts +10 -0
  230. package/dist/recipes/legacyRecipeCompat.js +131 -0
  231. package/dist/recipes/legacyRecipeCompat.js.map +1 -0
  232. package/dist/recipes/manifest.d.ts +47 -0
  233. package/dist/recipes/manifest.js +156 -0
  234. package/dist/recipes/manifest.js.map +1 -0
  235. package/dist/recipes/migrationWarnings.d.ts +12 -0
  236. package/dist/recipes/migrationWarnings.js +44 -0
  237. package/dist/recipes/migrationWarnings.js.map +1 -0
  238. package/dist/recipes/migrations/index.d.ts +24 -0
  239. package/dist/recipes/migrations/index.js +55 -0
  240. package/dist/recipes/migrations/index.js.map +1 -0
  241. package/dist/recipes/migrations/types.d.ts +28 -0
  242. package/dist/recipes/migrations/types.js +2 -0
  243. package/dist/recipes/migrations/types.js.map +1 -0
  244. package/dist/recipes/migrations/v1.d.ts +11 -0
  245. package/dist/recipes/migrations/v1.js +18 -0
  246. package/dist/recipes/migrations/v1.js.map +1 -0
  247. package/dist/recipes/names.d.ts +40 -0
  248. package/dist/recipes/names.js +66 -0
  249. package/dist/recipes/names.js.map +1 -0
  250. package/dist/recipes/nestedRecipeStep.d.ts +58 -0
  251. package/dist/recipes/nestedRecipeStep.js +95 -0
  252. package/dist/recipes/nestedRecipeStep.js.map +1 -0
  253. package/dist/recipes/outputRegistry.d.ts +28 -0
  254. package/dist/recipes/outputRegistry.js +52 -0
  255. package/dist/recipes/outputRegistry.js.map +1 -0
  256. package/dist/recipes/parser.js +4 -1
  257. package/dist/recipes/parser.js.map +1 -1
  258. package/dist/recipes/replayRun.d.ts +62 -0
  259. package/dist/recipes/replayRun.js +97 -0
  260. package/dist/recipes/replayRun.js.map +1 -0
  261. package/dist/recipes/resolveRecipePath.d.ts +69 -0
  262. package/dist/recipes/resolveRecipePath.js +202 -0
  263. package/dist/recipes/resolveRecipePath.js.map +1 -0
  264. package/dist/recipes/scheduler.d.ts +23 -7
  265. package/dist/recipes/scheduler.js +225 -45
  266. package/dist/recipes/scheduler.js.map +1 -1
  267. package/dist/recipes/schema.d.ts +17 -2
  268. package/dist/recipes/schemaGenerator.d.ts +28 -0
  269. package/dist/recipes/schemaGenerator.js +565 -0
  270. package/dist/recipes/schemaGenerator.js.map +1 -0
  271. package/dist/recipes/stepObservation.d.ts +44 -0
  272. package/dist/recipes/stepObservation.js +232 -0
  273. package/dist/recipes/stepObservation.js.map +1 -0
  274. package/dist/recipes/templateEngine.d.ts +62 -0
  275. package/dist/recipes/templateEngine.js +201 -0
  276. package/dist/recipes/templateEngine.js.map +1 -0
  277. package/dist/recipes/toolRegistry.d.ts +186 -0
  278. package/dist/recipes/toolRegistry.js +309 -0
  279. package/dist/recipes/toolRegistry.js.map +1 -0
  280. package/dist/recipes/tools/asana.d.ts +16 -0
  281. package/dist/recipes/tools/asana.js +524 -0
  282. package/dist/recipes/tools/asana.js.map +1 -0
  283. package/dist/recipes/tools/calendar.d.ts +6 -0
  284. package/dist/recipes/tools/calendar.js +61 -0
  285. package/dist/recipes/tools/calendar.js.map +1 -0
  286. package/dist/recipes/tools/confluence.d.ts +6 -0
  287. package/dist/recipes/tools/confluence.js +254 -0
  288. package/dist/recipes/tools/confluence.js.map +1 -0
  289. package/dist/recipes/tools/datadog.d.ts +6 -0
  290. package/dist/recipes/tools/datadog.js +239 -0
  291. package/dist/recipes/tools/datadog.js.map +1 -0
  292. package/dist/recipes/tools/diagnostics.d.ts +6 -0
  293. package/dist/recipes/tools/diagnostics.js +36 -0
  294. package/dist/recipes/tools/diagnostics.js.map +1 -0
  295. package/dist/recipes/tools/discord.d.ts +18 -0
  296. package/dist/recipes/tools/discord.js +254 -0
  297. package/dist/recipes/tools/discord.js.map +1 -0
  298. package/dist/recipes/tools/file.d.ts +12 -0
  299. package/dist/recipes/tools/file.js +174 -0
  300. package/dist/recipes/tools/file.js.map +1 -0
  301. package/dist/recipes/tools/git.d.ts +6 -0
  302. package/dist/recipes/tools/git.js +63 -0
  303. package/dist/recipes/tools/git.js.map +1 -0
  304. package/dist/recipes/tools/github.d.ts +6 -0
  305. package/dist/recipes/tools/github.js +116 -0
  306. package/dist/recipes/tools/github.js.map +1 -0
  307. package/dist/recipes/tools/gitlab.d.ts +11 -0
  308. package/dist/recipes/tools/gitlab.js +285 -0
  309. package/dist/recipes/tools/gitlab.js.map +1 -0
  310. package/dist/recipes/tools/gmail.d.ts +6 -0
  311. package/dist/recipes/tools/gmail.js +434 -0
  312. package/dist/recipes/tools/gmail.js.map +1 -0
  313. package/dist/recipes/tools/googleDrive.d.ts +1 -0
  314. package/dist/recipes/tools/googleDrive.js +55 -0
  315. package/dist/recipes/tools/googleDrive.js.map +1 -0
  316. package/dist/recipes/tools/hubspot.d.ts +6 -0
  317. package/dist/recipes/tools/hubspot.js +232 -0
  318. package/dist/recipes/tools/hubspot.js.map +1 -0
  319. package/dist/recipes/tools/index.d.ts +30 -0
  320. package/dist/recipes/tools/index.js +33 -0
  321. package/dist/recipes/tools/index.js.map +1 -0
  322. package/dist/recipes/tools/intercom.d.ts +6 -0
  323. package/dist/recipes/tools/intercom.js +226 -0
  324. package/dist/recipes/tools/intercom.js.map +1 -0
  325. package/dist/recipes/tools/jira.d.ts +14 -0
  326. package/dist/recipes/tools/jira.js +369 -0
  327. package/dist/recipes/tools/jira.js.map +1 -0
  328. package/dist/recipes/tools/linear.d.ts +7 -0
  329. package/dist/recipes/tools/linear.js +307 -0
  330. package/dist/recipes/tools/linear.js.map +1 -0
  331. package/dist/recipes/tools/meetingNotes.d.ts +21 -0
  332. package/dist/recipes/tools/meetingNotes.js +701 -0
  333. package/dist/recipes/tools/meetingNotes.js.map +1 -0
  334. package/dist/recipes/tools/notion.d.ts +6 -0
  335. package/dist/recipes/tools/notion.js +278 -0
  336. package/dist/recipes/tools/notion.js.map +1 -0
  337. package/dist/recipes/tools/pagerduty.d.ts +15 -0
  338. package/dist/recipes/tools/pagerduty.js +451 -0
  339. package/dist/recipes/tools/pagerduty.js.map +1 -0
  340. package/dist/recipes/tools/sentry.d.ts +12 -0
  341. package/dist/recipes/tools/sentry.js +73 -0
  342. package/dist/recipes/tools/sentry.js.map +1 -0
  343. package/dist/recipes/tools/slack.d.ts +6 -0
  344. package/dist/recipes/tools/slack.js +82 -0
  345. package/dist/recipes/tools/slack.js.map +1 -0
  346. package/dist/recipes/tools/stripe.d.ts +6 -0
  347. package/dist/recipes/tools/stripe.js +265 -0
  348. package/dist/recipes/tools/stripe.js.map +1 -0
  349. package/dist/recipes/tools/zendesk.d.ts +6 -0
  350. package/dist/recipes/tools/zendesk.js +245 -0
  351. package/dist/recipes/tools/zendesk.js.map +1 -0
  352. package/dist/recipes/validation.d.ts +13 -0
  353. package/dist/recipes/validation.js +617 -0
  354. package/dist/recipes/validation.js.map +1 -0
  355. package/dist/recipes/yamlRunner.d.ts +116 -1
  356. package/dist/recipes/yamlRunner.js +1000 -401
  357. package/dist/recipes/yamlRunner.js.map +1 -1
  358. package/dist/recipesHttp.d.ts +137 -6
  359. package/dist/recipesHttp.js +941 -29
  360. package/dist/recipesHttp.js.map +1 -1
  361. package/dist/riskTier.js +7 -1
  362. package/dist/riskTier.js.map +1 -1
  363. package/dist/runLog.d.ts +100 -1
  364. package/dist/runLog.js +258 -5
  365. package/dist/runLog.js.map +1 -1
  366. package/dist/schemas/dry-run-plan.v1.json +139 -0
  367. package/dist/schemas/recipe.v1.json +684 -0
  368. package/dist/server.d.ts +121 -8
  369. package/dist/server.js +538 -735
  370. package/dist/server.js.map +1 -1
  371. package/dist/ssrfGuard.d.ts +54 -0
  372. package/dist/ssrfGuard.js +122 -0
  373. package/dist/ssrfGuard.js.map +1 -0
  374. package/dist/streamableHttp.d.ts +39 -1
  375. package/dist/streamableHttp.js +128 -17
  376. package/dist/streamableHttp.js.map +1 -1
  377. package/dist/tokenUsageTracker.d.ts +33 -0
  378. package/dist/tokenUsageTracker.js +146 -0
  379. package/dist/tokenUsageTracker.js.map +1 -0
  380. package/dist/tools/activityLog.d.ts +2 -0
  381. package/dist/tools/addLinearComment.d.ts +1 -0
  382. package/dist/tools/addLinearComment.js +4 -2
  383. package/dist/tools/addLinearComment.js.map +1 -1
  384. package/dist/tools/batchLsp.d.ts +3 -0
  385. package/dist/tools/bridgeDoctor.d.ts +1 -0
  386. package/dist/tools/bridgeDoctor.js +2 -2
  387. package/dist/tools/bridgeDoctor.js.map +1 -1
  388. package/dist/tools/bridgeStatus.d.ts +1 -0
  389. package/dist/tools/cancelClaudeTask.d.ts +2 -0
  390. package/dist/tools/cancelClaudeTask.js +1 -0
  391. package/dist/tools/cancelClaudeTask.js.map +1 -1
  392. package/dist/tools/checkDocumentDirty.d.ts +1 -0
  393. package/dist/tools/clipboard.d.ts +2 -0
  394. package/dist/tools/closeTabs.d.ts +2 -0
  395. package/dist/tools/codeLens.d.ts +1 -0
  396. package/dist/tools/contextBundle.d.ts +1 -0
  397. package/dist/tools/createIssueFromAIComment.d.ts +1 -0
  398. package/dist/tools/createLinearIssue.d.ts +1 -0
  399. package/dist/tools/ctxGetTaskContext.d.ts +1 -0
  400. package/dist/tools/ctxQueryTraces.d.ts +1 -0
  401. package/dist/tools/ctxSaveTrace.d.ts +1 -0
  402. package/dist/tools/debug.d.ts +4 -0
  403. package/dist/tools/decorations.d.ts +2 -0
  404. package/dist/tools/documentLinks.d.ts +1 -0
  405. package/dist/tools/editText.d.ts +1 -0
  406. package/dist/tools/enrichCommit.d.ts +1 -0
  407. package/dist/tools/enrichStackTrace.d.ts +1 -0
  408. package/dist/tools/explainDiagnostic.d.ts +1 -0
  409. package/dist/tools/explainSymbol.d.ts +1 -0
  410. package/dist/tools/fetchCalendarEvents.d.ts +1 -0
  411. package/dist/tools/fetchGithubIssue.d.ts +1 -0
  412. package/dist/tools/fetchGithubPR.d.ts +1 -0
  413. package/dist/tools/fetchLinearIssue.d.ts +1 -0
  414. package/dist/tools/fetchSentryIssue.d.ts +1 -0
  415. package/dist/tools/fetchSlackProfile.d.ts +1 -0
  416. package/dist/tools/fetchSlackProfile.js +4 -1
  417. package/dist/tools/fetchSlackProfile.js.map +1 -1
  418. package/dist/tools/fileOperations.d.ts +3 -0
  419. package/dist/tools/fileWatcher.d.ts +2 -0
  420. package/dist/tools/findFiles.d.ts +1 -0
  421. package/dist/tools/findRelatedTests.d.ts +1 -0
  422. package/dist/tools/fixAllLintErrors.d.ts +1 -0
  423. package/dist/tools/foldingRanges.d.ts +1 -0
  424. package/dist/tools/formatDocument.d.ts +1 -0
  425. package/dist/tools/generateTests.d.ts +1 -0
  426. package/dist/tools/getAIComments.d.ts +1 -0
  427. package/dist/tools/getAnalyticsReport.d.ts +1 -0
  428. package/dist/tools/getArchitectureContext.d.ts +1 -0
  429. package/dist/tools/getBufferContent.d.ts +1 -0
  430. package/dist/tools/getChangeImpact.d.ts +1 -0
  431. package/dist/tools/getClaudeTaskStatus.d.ts +2 -0
  432. package/dist/tools/getClaudeTaskStatus.js +1 -0
  433. package/dist/tools/getClaudeTaskStatus.js.map +1 -1
  434. package/dist/tools/getCodeCoverage.d.ts +1 -0
  435. package/dist/tools/getCommitsForIssue.d.ts +1 -0
  436. package/dist/tools/getConnectorStatus.d.ts +1 -0
  437. package/dist/tools/getCurrentSelection.d.ts +2 -0
  438. package/dist/tools/getDebugState.d.ts +1 -0
  439. package/dist/tools/getDependencyTree.d.ts +1 -0
  440. package/dist/tools/getDiagnostics.d.ts +1 -0
  441. package/dist/tools/getDiffFromHandoff.d.ts +1 -0
  442. package/dist/tools/getDocumentSymbols.d.ts +25 -0
  443. package/dist/tools/getDocumentSymbols.js +74 -8
  444. package/dist/tools/getDocumentSymbols.js.map +1 -1
  445. package/dist/tools/getFileTree.d.ts +1 -0
  446. package/dist/tools/getGitDiff.d.ts +1 -0
  447. package/dist/tools/getGitHotspots.d.ts +1 -0
  448. package/dist/tools/getGitLog.d.ts +1 -0
  449. package/dist/tools/getGitStatus.d.ts +1 -0
  450. package/dist/tools/getImportTree.d.ts +1 -0
  451. package/dist/tools/getImportedSignatures.d.ts +1 -0
  452. package/dist/tools/getOpenEditors.d.ts +1 -0
  453. package/dist/tools/getPRTemplate.d.ts +1 -0
  454. package/dist/tools/getProjectContext.d.ts +1 -0
  455. package/dist/tools/getProjectInfo.d.ts +1 -0
  456. package/dist/tools/getSecurityAdvisories.d.ts +1 -0
  457. package/dist/tools/getSecurityAdvisories.js +10 -1
  458. package/dist/tools/getSecurityAdvisories.js.map +1 -1
  459. package/dist/tools/getSessionUsage.d.ts +4 -0
  460. package/dist/tools/getSessionUsage.js +3 -0
  461. package/dist/tools/getSessionUsage.js.map +1 -1
  462. package/dist/tools/getSymbolHistory.d.ts +1 -0
  463. package/dist/tools/getToolCapabilities.d.ts +1 -0
  464. package/dist/tools/getTypeSignature.d.ts +1 -0
  465. package/dist/tools/getWorkspaceFolders.d.ts +1 -0
  466. package/dist/tools/getWorkspaceSettings.d.ts +1 -0
  467. package/dist/tools/gitHistory.d.ts +2 -0
  468. package/dist/tools/gitWrite.d.ts +11 -0
  469. package/dist/tools/github/actions.d.ts +2 -0
  470. package/dist/tools/github/actions.js +4 -2
  471. package/dist/tools/github/actions.js.map +1 -1
  472. package/dist/tools/github/composite.d.ts +342 -0
  473. package/dist/tools/github/composite.js +343 -0
  474. package/dist/tools/github/composite.js.map +1 -0
  475. package/dist/tools/github/index.d.ts +1 -0
  476. package/dist/tools/github/index.js +1 -0
  477. package/dist/tools/github/index.js.map +1 -1
  478. package/dist/tools/github/issues.d.ts +4 -0
  479. package/dist/tools/github/issues.js +8 -4
  480. package/dist/tools/github/issues.js.map +1 -1
  481. package/dist/tools/github/pr.d.ts +7 -0
  482. package/dist/tools/github/pr.js +50 -12
  483. package/dist/tools/github/pr.js.map +1 -1
  484. package/dist/tools/handoffNote.d.ts +4 -0
  485. package/dist/tools/handoffNote.js +2 -0
  486. package/dist/tools/handoffNote.js.map +1 -1
  487. package/dist/tools/hoverAtCursor.d.ts +1 -0
  488. package/dist/tools/httpClient.d.ts +2 -0
  489. package/dist/tools/index.d.ts +8 -0
  490. package/dist/tools/index.js +47 -8
  491. package/dist/tools/index.js.map +1 -1
  492. package/dist/tools/inlayHints.d.ts +1 -0
  493. package/dist/tools/launchQuickTask.d.ts +2 -0
  494. package/dist/tools/launchQuickTask.js +1 -0
  495. package/dist/tools/launchQuickTask.js.map +1 -1
  496. package/dist/tools/listClaudeTasks.d.ts +2 -0
  497. package/dist/tools/listClaudeTasks.js +1 -0
  498. package/dist/tools/listClaudeTasks.js.map +1 -1
  499. package/dist/tools/listTerminals.d.ts +1 -0
  500. package/dist/tools/lsp.d.ts +14 -0
  501. package/dist/tools/navigateToSymbolByName.d.ts +1 -0
  502. package/dist/tools/openDiff.d.ts +1 -0
  503. package/dist/tools/openFile.d.ts +1 -0
  504. package/dist/tools/openInBrowser.d.ts +1 -0
  505. package/dist/tools/organizeImports.d.ts +1 -0
  506. package/dist/tools/performanceReport.d.ts +1 -0
  507. package/dist/tools/planPersistence.d.ts +5 -0
  508. package/dist/tools/previewEdit.d.ts +1 -0
  509. package/dist/tools/refactorAnalyze.d.ts +1 -0
  510. package/dist/tools/refactorPreview.d.ts +2 -0
  511. package/dist/tools/refactorPreview.js +1 -0
  512. package/dist/tools/refactorPreview.js.map +1 -1
  513. package/dist/tools/replaceBlock.d.ts +1 -0
  514. package/dist/tools/resumeClaudeTask.d.ts +2 -0
  515. package/dist/tools/resumeClaudeTask.js +1 -0
  516. package/dist/tools/resumeClaudeTask.js.map +1 -1
  517. package/dist/tools/runClaudeTask.d.ts +2 -0
  518. package/dist/tools/runClaudeTask.js +1 -0
  519. package/dist/tools/runClaudeTask.js.map +1 -1
  520. package/dist/tools/runCommand.d.ts +1 -0
  521. package/dist/tools/runTests.d.ts +1 -0
  522. package/dist/tools/saveDocument.d.ts +1 -0
  523. package/dist/tools/screenshotAndAnnotate.d.ts +1 -0
  524. package/dist/tools/searchAndReplace.d.ts +1 -0
  525. package/dist/tools/searchTools.d.ts +1 -0
  526. package/dist/tools/searchTools.js +1 -1
  527. package/dist/tools/searchTools.js.map +1 -1
  528. package/dist/tools/searchWorkspace.d.ts +1 -0
  529. package/dist/tools/selectionRanges.d.ts +1 -0
  530. package/dist/tools/semanticTokens.d.ts +1 -0
  531. package/dist/tools/setActiveWorkspaceFolder.d.ts +1 -0
  532. package/dist/tools/signatureHelp.d.ts +1 -0
  533. package/dist/tools/slackListChannels.d.ts +1 -0
  534. package/dist/tools/slackListChannels.js.map +1 -1
  535. package/dist/tools/slackPostMessage.d.ts +1 -0
  536. package/dist/tools/slackPostMessage.js +11 -6
  537. package/dist/tools/slackPostMessage.js.map +1 -1
  538. package/dist/tools/terminal.d.ts +6 -0
  539. package/dist/tools/testTraceToSource.d.ts +1 -0
  540. package/dist/tools/testTraceToSource.js +2 -2
  541. package/dist/tools/testTraceToSource.js.map +1 -1
  542. package/dist/tools/transaction.d.ts +23 -0
  543. package/dist/tools/transaction.js +29 -0
  544. package/dist/tools/transaction.js.map +1 -1
  545. package/dist/tools/typeHierarchy.d.ts +1 -0
  546. package/dist/tools/updateLinearIssue.d.ts +1 -0
  547. package/dist/tools/updateLinearIssue.js +20 -6
  548. package/dist/tools/updateLinearIssue.js.map +1 -1
  549. package/dist/tools/utils.d.ts +2 -0
  550. package/dist/tools/utils.js.map +1 -1
  551. package/dist/tools/vscodeCommands.d.ts +2 -0
  552. package/dist/tools/vscodeTasks.d.ts +2 -0
  553. package/dist/tools/workspaceSettings.d.ts +1 -0
  554. package/dist/traceEncryption.d.ts +46 -0
  555. package/dist/traceEncryption.js +124 -0
  556. package/dist/traceEncryption.js.map +1 -0
  557. package/dist/transport.d.ts +46 -1
  558. package/dist/transport.js +173 -19
  559. package/dist/transport.js.map +1 -1
  560. package/package.json +30 -8
  561. package/scripts/mcp-stdio-shim.cjs +19 -3
  562. package/scripts/start-all.sh +30 -1
  563. package/templates/automation-policies/recipe-authoring.json +25 -0
  564. package/templates/automation-policy.example.json +6 -0
  565. package/templates/co.patchwork-os.bridge.plist +34 -0
  566. package/templates/policies/README.md +72 -0
  567. package/templates/policies/conservative.json +14 -0
  568. package/templates/policies/developer.json +14 -0
  569. package/templates/policies/headless-ci.json +24 -0
  570. package/templates/policies/personal-assistant.json +15 -0
  571. package/templates/policies/regulated-industry.json +18 -0
  572. package/templates/recipes/lint-on-save.yaml +1 -2
  573. package/templates/recipes/morning-brief-slack.yaml +57 -0
  574. package/templates/recipes/morning-brief.yaml +2 -2
  575. package/templates/recipes/project-health-check.yaml +50 -0
  576. package/templates/recipes/webhook/README.md +70 -0
  577. package/templates/recipes/webhook/capture-thought.yaml +26 -0
  578. package/templates/recipes/webhook/customer-escalation.yaml +49 -0
  579. package/templates/recipes/webhook/incident-intake.yaml +46 -0
  580. package/templates/recipes/webhook/meeting-prep.yaml +48 -0
  581. package/templates/recipes/webhook/morning-brief.yaml +57 -0
@@ -0,0 +1,1359 @@
1
+ /**
2
+ * Recipe CLI commands — new, lint, test, watch, record, fmt
3
+ *
4
+ * Implements the A2 CLI UX milestone for recipe authoring.
5
+ */
6
+ import { existsSync, mkdirSync, readFileSync, statSync, watch, writeFileSync, } from "node:fs";
7
+ import os from "node:os";
8
+ import { basename, dirname, join, resolve } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
11
+ import "../recipes/tools/index.js";
12
+ import { loadFixtureLibrary } from "../connectors/fixtureLibrary.js";
13
+ import { MockConnector } from "../connectors/mockConnector.js";
14
+ import { defaultDeprecationWarn, normalizeRecipeForRuntime, } from "../recipes/legacyRecipeCompat.js";
15
+ import { tryResolveRecipePath } from "../recipes/resolveRecipePath.js";
16
+ import { generateSchemaSet, writeSchemas } from "../recipes/schemaGenerator.js";
17
+ import { getTool, isConnectorNamespace, seedToolOutputPreviewContext, } from "../recipes/toolRegistry.js";
18
+ import { validateRecipeDefinition, } from "../recipes/validation.js";
19
+ import { buildChainedDeps, dispatchRecipe, loadYamlRecipe, render, runYamlRecipe, } from "../recipes/yamlRunner.js";
20
+ import { findYamlRecipePath } from "../recipesHttp.js";
21
+ import { findInstalledRecipeEntrypoint } from "./recipeInstall.js";
22
+ const RECIPES_DIR = join(os.homedir(), ".patchwork", "recipes");
23
+ const FIXTURES_DIR = join(os.homedir(), ".patchwork", "fixtures");
24
+ const RECIPE_SCHEMA_HEADER = "# yaml-language-server: $schema=https://raw.githubusercontent.com/patchworkos/recipes/main/schema/recipe.v1.json";
25
+ const RECIPE_API_VERSION = "patchwork.sh/v1";
26
+ // ============================================================================
27
+ // patchwork recipe new
28
+ // ============================================================================
29
+ const TEMPLATES = {
30
+ minimal: `apiVersion: ${RECIPE_API_VERSION}
31
+ name: {{name}}
32
+ description: {{description}}
33
+ trigger:
34
+ type: manual
35
+ steps:
36
+ - tool: file.write
37
+ path: ~/.patchwork/inbox/{{name}}.md
38
+ content: "Hello from {{name}}\\n"
39
+ `,
40
+ daily: `apiVersion: ${RECIPE_API_VERSION}
41
+ name: {{name}}
42
+ description: {{description}}
43
+ trigger:
44
+ type: cron
45
+ at: "0 9 * * 1-5"
46
+ steps:
47
+ - tool: git.log_since
48
+ since: "24h"
49
+ into: commits
50
+ - agent:
51
+ prompt: |
52
+ Summarize these commits for a daily standup:
53
+ {{commits}}
54
+ into: summary
55
+ - tool: file.write
56
+ path: ~/.patchwork/inbox/{{name}}-{{date}}.md
57
+ content: "# {{name}}\\n\\n{{summary}}\\n"
58
+ `,
59
+ inbox: `apiVersion: ${RECIPE_API_VERSION}
60
+ name: {{name}}
61
+ description: {{description}}
62
+ trigger:
63
+ type: manual
64
+ steps:
65
+ - tool: gmail.fetch_unread
66
+ since: "24h"
67
+ max: 20
68
+ into: unread
69
+ - tool: github.list_issues
70
+ assignee: "@me"
71
+ max: 10
72
+ into: issues
73
+ - agent:
74
+ prompt: |
75
+ Summarize my inbox. Unread emails: {{unread}}.
76
+ Assigned issues: {{issues}}.
77
+ into: summary
78
+ - tool: file.write
79
+ path: ~/.patchwork/inbox/{{name}}-{{date}}.md
80
+ content: "# {{name}}\\n\\n{{summary}}\\n"
81
+ `,
82
+ };
83
+ export function runNew(options) {
84
+ if (!options.name) {
85
+ throw new Error("Recipe name is required");
86
+ }
87
+ if (!options.description) {
88
+ throw new Error("Recipe description is required");
89
+ }
90
+ const templateKey = options.template ?? "minimal";
91
+ const template = TEMPLATES[templateKey];
92
+ if (!template) {
93
+ throw new Error(`Unknown template: "${templateKey}". ` +
94
+ `Available: ${Object.keys(TEMPLATES).join(", ")}`);
95
+ }
96
+ const today = new Date().toISOString().split("T")[0] ?? "";
97
+ const body = template
98
+ .replace(/\{\{name\}\}/g, options.name)
99
+ .replace(/\{\{description\}\}/g, options.description)
100
+ .replace(/\{\{date\}\}/g, today);
101
+ const content = `${RECIPE_SCHEMA_HEADER}\n${body}`;
102
+ const outputDir = options.outputDir ?? RECIPES_DIR;
103
+ if (!existsSync(outputDir)) {
104
+ mkdirSync(outputDir, { recursive: true });
105
+ }
106
+ const outputPath = join(outputDir, `${options.name}.yaml`);
107
+ if (existsSync(outputPath)) {
108
+ throw new Error(`Recipe already exists: ${outputPath}`);
109
+ }
110
+ writeFileSync(outputPath, content);
111
+ return { path: outputPath, content };
112
+ }
113
+ export function listTemplates() {
114
+ return Object.keys(TEMPLATES);
115
+ }
116
+ export async function runSchema(outputDir) {
117
+ const resolvedOutputDir = resolve(outputDir);
118
+ const schemas = generateSchemaSet();
119
+ const filesWritten = [];
120
+ await writeSchemas(resolvedOutputDir, schemas, (filePath, content) => {
121
+ const dir = dirname(filePath);
122
+ if (!existsSync(dir)) {
123
+ mkdirSync(dir, { recursive: true });
124
+ }
125
+ writeFileSync(filePath, content);
126
+ filesWritten.push(filePath);
127
+ });
128
+ return {
129
+ outputDir: resolvedOutputDir,
130
+ filesWritten,
131
+ };
132
+ }
133
+ /**
134
+ * Lint a recipe file against the schema.
135
+ * Falls back to basic YAML parsing if schema linting is disabled.
136
+ */
137
+ export function runLint(recipePath) {
138
+ // Check file exists
139
+ if (!existsSync(recipePath)) {
140
+ return {
141
+ valid: false,
142
+ issues: [{ level: "error", message: `File not found: ${recipePath}` }],
143
+ warnings: 0,
144
+ errors: 1,
145
+ };
146
+ }
147
+ let content;
148
+ try {
149
+ content = readFileSync(recipePath, "utf-8");
150
+ }
151
+ catch (err) {
152
+ return {
153
+ valid: false,
154
+ issues: [
155
+ {
156
+ level: "error",
157
+ message: `Failed to read file: ${err instanceof Error ? err.message : String(err)}`,
158
+ },
159
+ ],
160
+ warnings: 0,
161
+ errors: 1,
162
+ };
163
+ }
164
+ let parsed;
165
+ try {
166
+ parsed = parseYaml(content);
167
+ }
168
+ catch (err) {
169
+ return {
170
+ valid: false,
171
+ issues: [
172
+ {
173
+ level: "error",
174
+ message: `YAML parse error: ${err instanceof Error ? err.message : String(err)}`,
175
+ },
176
+ ],
177
+ warnings: 0,
178
+ errors: 1,
179
+ };
180
+ }
181
+ const result = validateRecipeDefinition(parsed);
182
+ // For chained recipes, check that chain: file references resolve on disk.
183
+ const chainIssues = lintChainRefs(parsed, recipePath);
184
+ if (chainIssues.length > 0) {
185
+ result.issues.push(...chainIssues);
186
+ result.errors += chainIssues.filter((i) => i.level === "error").length;
187
+ result.warnings += chainIssues.filter((i) => i.level === "warning").length;
188
+ if (result.errors > 0) {
189
+ result.valid = false;
190
+ }
191
+ }
192
+ return result;
193
+ }
194
+ /**
195
+ * Walk chained recipe steps, check that chain:/recipe: refs resolve on disk,
196
+ * and recursively lint any child recipe that does resolve.
197
+ *
198
+ * `visited` tracks absolute paths already linted in this call chain to prevent
199
+ * infinite recursion when two recipes chain each other.
200
+ */
201
+ function lintChainRefs(parsed, recipePath, visited = new Set()) {
202
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
203
+ return [];
204
+ const r = parsed;
205
+ const trigger = r.trigger && typeof r.trigger === "object"
206
+ ? r.trigger
207
+ : undefined;
208
+ if (trigger?.type !== "chained")
209
+ return [];
210
+ const steps = Array.isArray(r.steps)
211
+ ? r.steps
212
+ : [];
213
+ const recipeDir = dirname(recipePath);
214
+ const issues = [];
215
+ // Mark the current recipe as visited before descending.
216
+ const absPath = resolve(recipePath);
217
+ visited.add(absPath);
218
+ for (let i = 0; i < steps.length; i++) {
219
+ const step = steps[i];
220
+ if (!step)
221
+ continue;
222
+ issues.push(...lintStep(step, i + 1, recipeDir, visited));
223
+ }
224
+ return issues;
225
+ }
226
+ /**
227
+ * Check a single step (or recurse into its parallel: children).
228
+ * `stepLabel` is the 1-based position string used in issue messages.
229
+ */
230
+ function lintStep(step, stepLabel, recipeDir, visited) {
231
+ const issues = [];
232
+ // Recurse into parallel: groups — each child is checked independently.
233
+ if (Array.isArray(step.parallel)) {
234
+ for (let j = 0; j < step.parallel.length; j++) {
235
+ const child = step.parallel[j];
236
+ if (!child || typeof child !== "object" || Array.isArray(child))
237
+ continue;
238
+ issues.push(...lintStep(child, stepLabel, recipeDir, visited));
239
+ }
240
+ return issues;
241
+ }
242
+ const ref = typeof step.chain === "string"
243
+ ? step.chain
244
+ : typeof step.recipe === "string"
245
+ ? step.recipe
246
+ : null;
247
+ if (!ref)
248
+ return issues;
249
+ const field = typeof step.chain === "string" ? "chain" : "recipe";
250
+ // Refs that look like file paths (extension or separator) → resolve relative to recipe dir.
251
+ const looksLikePath = /\.ya?ml$/i.test(ref) ||
252
+ ref.startsWith("./") ||
253
+ ref.startsWith("../") ||
254
+ /[\\/]/.test(ref);
255
+ if (looksLikePath) {
256
+ const resolved = /^\//.test(ref) ? ref : resolve(recipeDir, ref);
257
+ const candidates = /\.ya?ml$/i.test(resolved)
258
+ ? [resolved]
259
+ : [`${resolved}.yaml`, `${resolved}.yml`, resolved];
260
+ const childPath = candidates.find(existsSync) ?? null;
261
+ if (!childPath) {
262
+ issues.push({
263
+ level: "error",
264
+ message: `Step ${stepLabel}: '${field}: ${ref}' — file not found relative to recipe directory (${recipeDir})`,
265
+ });
266
+ return issues;
267
+ }
268
+ issues.push(...lintChildRecipe(childPath, field, ref, stepLabel, visited));
269
+ return issues;
270
+ }
271
+ // Named ref (no extension, no separator) → check ~/.patchwork/recipes/.
272
+ // Emit a warning rather than error: the recipe may be installed on the
273
+ // deploy target but not the author's machine.
274
+ if (existsSync(RECIPES_DIR)) {
275
+ let found = null;
276
+ try {
277
+ found =
278
+ findYamlRecipePath(RECIPES_DIR, ref) ??
279
+ (existsSync(join(RECIPES_DIR, ref)) ? join(RECIPES_DIR, ref) : null);
280
+ }
281
+ catch (err) {
282
+ issues.push({
283
+ level: "error",
284
+ message: `Step ${stepLabel}: '${field}: ${ref}' — ${err instanceof Error ? err.message : String(err)}`,
285
+ });
286
+ return issues;
287
+ }
288
+ if (!found) {
289
+ issues.push({
290
+ level: "warning",
291
+ message: `Step ${stepLabel}: '${field}: ${ref}' — recipe not found in ${RECIPES_DIR}`,
292
+ });
293
+ }
294
+ else {
295
+ issues.push(...lintChildRecipe(found, field, ref, stepLabel, visited));
296
+ }
297
+ }
298
+ return issues;
299
+ }
300
+ /**
301
+ * Read, parse, and validate a resolved child recipe path. Skips the file if
302
+ * it has already been visited (cycle). Issues are prefixed with the parent
303
+ * step context so the author knows where the problem originates.
304
+ */
305
+ function lintChildRecipe(childPath, field, ref, stepNumber, visited) {
306
+ const absChild = resolve(childPath);
307
+ if (visited.has(absChild))
308
+ return []; // cycle — already linted
309
+ let childParsed;
310
+ try {
311
+ childParsed = parseYaml(readFileSync(childPath, "utf-8"));
312
+ }
313
+ catch (err) {
314
+ return [
315
+ {
316
+ level: "error",
317
+ message: `Step ${stepNumber}: '${field}: ${ref}' — could not read child recipe: ${err instanceof Error ? err.message : String(err)}`,
318
+ },
319
+ ];
320
+ }
321
+ const childResult = validateRecipeDefinition(childParsed);
322
+ const childChainIssues = lintChainRefs(childParsed, childPath, visited);
323
+ return [
324
+ ...childResult.issues.map((issue) => ({
325
+ ...issue,
326
+ message: `Step ${stepNumber}: '${field}: ${ref}' — child recipe invalid: ${issue.message}`,
327
+ })),
328
+ ...childChainIssues.map((issue) => ({
329
+ ...issue,
330
+ message: `Step ${stepNumber}: '${field}: ${ref}' — ${issue.message}`,
331
+ })),
332
+ ];
333
+ }
334
+ /**
335
+ * Format/normalize a recipe file.
336
+ * - Normalizes YAML formatting
337
+ * - Sorts keys in consistent order
338
+ * - Validates and re-serializes
339
+ */
340
+ export function runFmt(recipePath, options = {}) {
341
+ const content = readFileSync(recipePath, "utf-8");
342
+ const { header: schemaHeader } = extractSchemaHeader(content);
343
+ const recipe = normalizeRecipeForRuntime(parseYaml(content), defaultDeprecationWarn);
344
+ // Normalize key order
345
+ const normalized = {};
346
+ const keyOrder = [
347
+ "apiVersion",
348
+ "version",
349
+ "name",
350
+ "description",
351
+ "trigger",
352
+ "context",
353
+ "steps",
354
+ "expect",
355
+ "output",
356
+ "on_error",
357
+ ];
358
+ for (const key of keyOrder) {
359
+ if (key in recipe) {
360
+ normalized[key] = recipe[key];
361
+ }
362
+ }
363
+ // Add any extra keys at the end
364
+ for (const key of Object.keys(recipe)) {
365
+ if (!keyOrder.includes(key)) {
366
+ normalized[key] = recipe[key];
367
+ }
368
+ }
369
+ // Re-serialize with consistent formatting
370
+ const formattedBody = stringifyYaml(normalized, {
371
+ indent: 2,
372
+ lineWidth: 100,
373
+ });
374
+ const formatted = schemaHeader
375
+ ? `${schemaHeader}\n${formattedBody}`
376
+ : formattedBody;
377
+ const changed = formatted.trim() !== content.trim();
378
+ if (!options.check) {
379
+ writeFileSync(recipePath, formatted);
380
+ }
381
+ return { formatted, changed };
382
+ }
383
+ /**
384
+ * Watch a recipe file and re-run `runFmt` on every save (debounced).
385
+ * Mirrors runPreflightWatch / runTestWatch — composes runWatch + runFmt.
386
+ * Returns a stop function.
387
+ */
388
+ export function runFmtWatch(options) {
389
+ const { recipePath, check, onResult, onError, debounceMs, watchFactory } = options;
390
+ return runWatch({
391
+ recipePath,
392
+ onChange: async () => {
393
+ const result = runFmt(recipePath, { check });
394
+ await onResult(result);
395
+ },
396
+ ...(onError ? { onError } : {}),
397
+ ...(debounceMs !== undefined ? { debounceMs } : {}),
398
+ ...(watchFactory ? { watchFactory } : {}),
399
+ });
400
+ }
401
+ function extractSchemaHeader(content) {
402
+ if (content.startsWith(`${RECIPE_SCHEMA_HEADER}\n`)) {
403
+ return { header: RECIPE_SCHEMA_HEADER };
404
+ }
405
+ return {};
406
+ }
407
+ export async function runRecipe(recipeRef, options = {}) {
408
+ const recipePath = resolveRecipePath(recipeRef);
409
+ const recipe = loadYamlRecipe(recipePath);
410
+ const triggerType = recipe.trigger?.type;
411
+ if (options.step && triggerType === "chained") {
412
+ throw new Error(`Single-step execution is not supported for chained recipes: ${recipe.name}`);
413
+ }
414
+ const selection = options.step
415
+ ? selectRecipeStep(recipe, options.step)
416
+ : undefined;
417
+ const recipeToRun = selection
418
+ ? { ...recipe, steps: [selection.step] }
419
+ : recipe;
420
+ const runnerDeps = {
421
+ ...options.deps,
422
+ workdir: options.workdir ?? options.deps?.workdir ?? process.cwd(),
423
+ };
424
+ if (options.dryRun) {
425
+ throw new Error("runRecipeDryPlan must be used for dry-run execution");
426
+ }
427
+ const result = await dispatchRecipe(recipeToRun, {
428
+ ...runnerDeps,
429
+ chainedDeps: buildChainedDeps(runnerDeps),
430
+ chainedOptions: { sourcePath: recipePath },
431
+ }, options.vars ?? {});
432
+ return {
433
+ recipe,
434
+ recipePath,
435
+ result,
436
+ ...(selection
437
+ ? {
438
+ stepSelection: {
439
+ query: selection.query,
440
+ matchedBy: selection.matchedBy,
441
+ matchedValue: selection.matchedValue,
442
+ },
443
+ }
444
+ : {}),
445
+ };
446
+ }
447
+ export function summarizeRecipeExecution(result) {
448
+ if ("stepsRun" in result) {
449
+ return {
450
+ ok: !result.errorMessage,
451
+ steps: result.stepsRun,
452
+ outputs: result.outputs,
453
+ ...(result.errorMessage ? { errorMessage: result.errorMessage } : {}),
454
+ };
455
+ }
456
+ return {
457
+ ok: result.success,
458
+ steps: result.summary.total,
459
+ outputs: [],
460
+ ...(result.errorMessage ? { errorMessage: result.errorMessage } : {}),
461
+ failed: result.summary.failed,
462
+ skipped: result.summary.skipped,
463
+ };
464
+ }
465
+ /**
466
+ * Normalize either a yamlRunner RunResult or a chainedRunner ChainedRunResult
467
+ * into the RunStepResult[] shape expected by RecipeRunLog.appendDirect.
468
+ * Returns undefined when the result has no step-level detail.
469
+ */
470
+ export function extractRunLogStepResults(result) {
471
+ if ("stepsRun" in result) {
472
+ // yamlRunner: stepResults is already StepResult[]
473
+ if (!Array.isArray(result.stepResults))
474
+ return undefined;
475
+ return result.stepResults.map((s) => ({
476
+ id: s.id,
477
+ ...(s.tool ? { tool: s.tool } : {}),
478
+ status: s.status,
479
+ ...(s.error ? { error: s.error } : {}),
480
+ durationMs: s.durationMs,
481
+ }));
482
+ }
483
+ // chainedRunner: stepResults is Map<string, ChainedStepRunResult>
484
+ return [...result.stepResults.entries()].map(([id, s]) => ({
485
+ id,
486
+ status: s.skipped ? "skipped" : s.success ? "ok" : "error",
487
+ durationMs: s.durationMs ?? 0,
488
+ ...(s.error ? { error: s.error.message } : {}),
489
+ }));
490
+ }
491
+ export function formatRunReport(result, recipeName) {
492
+ const lines = [];
493
+ const hr = "─".repeat(48);
494
+ if ("stepsRun" in result) {
495
+ // Simple (non-chained) recipe — compact summary
496
+ const ok = !result.errorMessage;
497
+ lines.push(`${ok ? "✓" : "✗"} ${recipeName} — ${result.stepsRun} step(s)`);
498
+ if (result.outputs.length > 0) {
499
+ for (const o of result.outputs)
500
+ lines.push(` → ${o}`);
501
+ }
502
+ if (result.errorMessage)
503
+ lines.push(` Error: ${result.errorMessage}`);
504
+ return lines.join("\n");
505
+ }
506
+ // Chained recipe — per-step table
507
+ const { stepResults, summary } = result;
508
+ const overallOk = result.success;
509
+ lines.push(hr);
510
+ lines.push(`Recipe: ${recipeName}`);
511
+ lines.push(hr);
512
+ for (const [id, step] of stepResults) {
513
+ const icon = step.skipped ? "↷" : step.success ? "✓" : "✗";
514
+ const dur = step.durationMs !== undefined ? ` (${step.durationMs}ms)` : "";
515
+ const err = step.error ? ` → ${step.error.message}` : "";
516
+ lines.push(` ${icon} ${id}${dur}${err}`);
517
+ }
518
+ lines.push(hr);
519
+ const parts = [`${summary.succeeded} ok`];
520
+ if (summary.skipped > 0)
521
+ parts.push(`${summary.skipped} skipped`);
522
+ if (summary.failed > 0)
523
+ parts.push(`${summary.failed} failed`);
524
+ lines.push(`${overallOk ? "✓" : "✗"} ${parts.join(" · ")}`);
525
+ return lines.join("\n");
526
+ }
527
+ export async function runWatchedRecipe(recipePath, options = {}) {
528
+ const lint = runLint(recipePath);
529
+ if (!lint.valid) {
530
+ return { lint };
531
+ }
532
+ const run = await runRecipe(recipePath, options);
533
+ return {
534
+ lint,
535
+ run,
536
+ summary: summarizeRecipeExecution(run.result),
537
+ };
538
+ }
539
+ /**
540
+ * Stable JSON schema version for machine-readable dry-run plans.
541
+ * Bump on breaking shape changes; consumers (dashboard run timeline, external tools)
542
+ * should gate on this field.
543
+ */
544
+ export const DRY_RUN_PLAN_SCHEMA_VERSION = 1;
545
+ function enrichStepFromRegistry(step) {
546
+ if (step.type !== "tool" || !step.tool) {
547
+ return step;
548
+ }
549
+ const namespace = step.tool.split(".")[0];
550
+ const registered = getTool(step.tool);
551
+ const enriched = { ...step };
552
+ if (namespace)
553
+ enriched.namespace = namespace;
554
+ enriched.resolved = Boolean(registered);
555
+ if (registered) {
556
+ enriched.isWrite = registered.isWrite;
557
+ enriched.isConnector = registered.isConnector === true;
558
+ if (enriched.risk === undefined) {
559
+ enriched.risk = registered.riskDefault;
560
+ }
561
+ }
562
+ return enriched;
563
+ }
564
+ function summarizePlanSteps(steps) {
565
+ const connectors = new Set();
566
+ let hasWrite = false;
567
+ for (const step of steps) {
568
+ if (step.isConnector && step.namespace)
569
+ connectors.add(step.namespace);
570
+ if (step.isWrite)
571
+ hasWrite = true;
572
+ // F-07 fix: nested-recipe writes detected via recursion count too.
573
+ if (step.nestedWriteCount && step.nestedWriteCount > 0)
574
+ hasWrite = true;
575
+ }
576
+ return {
577
+ connectorNamespaces: [...connectors].sort(),
578
+ hasWriteSteps: hasWrite,
579
+ };
580
+ }
581
+ /**
582
+ * F-07 fix: hard cap on nested-recipe recursion depth used by the dry-plan
583
+ * write detector. The chained recipe schema's `maxDepth` (default 3) is the
584
+ * runtime cap; the dry-plan applies `min(recipe.maxDepth ?? 3, MAX_DEPTH)`
585
+ * so a malicious or buggy recipe can't make the planner walk an unbounded
586
+ * tree. Per PLAN-MASTER-V2.md (R2-H1), 5 is the hard cap.
587
+ */
588
+ const F07_MAX_NESTED_DEPTH = 5;
589
+ /**
590
+ * F-07 fix: resolve a nested-recipe reference (bare name or path-shape)
591
+ * to an absolute file path the dry-plan can load synchronously. Mirrors
592
+ * the resolution logic in `yamlRunner.ts`'s async `loadNestedRecipe` but
593
+ * keeps to the synchronous subset (no symlink walks, no async I/O) since
594
+ * the dry-plan is itself synchronous after the chained-runner import.
595
+ *
596
+ * Returns null if the candidate cannot be resolved within the allowed
597
+ * roots — the recursion treats null as "unknown writes" (omits the
598
+ * `nestedWriteCount` field) so a missing reference does not falsely mark
599
+ * the parent as `hasWriteSteps:false` when it might write.
600
+ *
601
+ * Allowed roots:
602
+ * - parent recipe's directory (for relative `recipe: ./inner.yaml`)
603
+ * - `~/.patchwork/recipes/` (for bare `recipe: branch-health`)
604
+ *
605
+ * The bundled-templates dir is intentionally NOT searched here — bundled
606
+ * templates are vendor-controlled, the dry-plan recursion is for user
607
+ * recipes that may legitimately reference user-installed sub-recipes.
608
+ */
609
+ function resolveNestedRecipeForDryPlan(ref, parentRecipePath) {
610
+ if (typeof ref !== "string" || ref.length === 0)
611
+ return null;
612
+ if (ref.includes("\x00"))
613
+ return null;
614
+ // node:path / node:os / node:fs are imported at the top of this file
615
+ // (see resolve, dirname, join above). statSync is not currently in scope
616
+ // so we re-pull from the already-imported namespace via the bare module
617
+ // — TS resolves this through the same package that `import os from "node:os"`
618
+ // uses. No dynamic require: this stays synchronous and ESM-friendly.
619
+ const parentDir = dirname(parentRecipePath);
620
+ const userRecipesDir = join(os.homedir(), ".patchwork", "recipes");
621
+ const pathLike = /^([./]|\.\.[/\\]|[A-Za-z]:[/\\])/.test(ref) ||
622
+ /[\\/]/.test(ref) ||
623
+ /\.ya?ml$/i.test(ref);
624
+ const candidates = [];
625
+ if (pathLike) {
626
+ const base = /^([./]|[A-Za-z]:[/\\])/.test(ref)
627
+ ? resolve(parentDir, ref)
628
+ : resolve(ref);
629
+ // resolve() already absolutized; if it was already absolute we re-resolve.
630
+ const absolute = resolve(base);
631
+ if (/\.ya?ml$/i.test(absolute)) {
632
+ candidates.push(absolute);
633
+ }
634
+ else {
635
+ candidates.push(`${absolute}.yaml`, `${absolute}.yml`, absolute);
636
+ }
637
+ }
638
+ else {
639
+ candidates.push(join(userRecipesDir, `${ref}.yaml`), join(userRecipesDir, `${ref}.yml`));
640
+ }
641
+ const allowedRoots = [parentDir, userRecipesDir];
642
+ const isInside = (file, root) => {
643
+ const rf = resolve(file);
644
+ const rr = resolve(root);
645
+ return rf === rr || rf.startsWith(`${rr}/`) || rf.startsWith(`${rr}\\`);
646
+ };
647
+ for (const candidate of candidates) {
648
+ const inJail = allowedRoots.some((root) => isInside(candidate, root));
649
+ if (!inJail)
650
+ continue;
651
+ try {
652
+ if (existsSync(candidate)) {
653
+ const st = statSync(candidate);
654
+ if (st.isFile())
655
+ return candidate;
656
+ }
657
+ }
658
+ catch {
659
+ /* not found; try next */
660
+ }
661
+ }
662
+ return null;
663
+ }
664
+ /**
665
+ * F-07 fix: recursively resolve nested-recipe steps in a chained dry-plan
666
+ * to detect writes. Mutates `steps` in place to set `nestedWriteCount` for
667
+ * any `type === "recipe"` step whose nested recipe could be loaded.
668
+ *
669
+ * Visited set prevents cycles (A → B → A) from looping; cycles count as
670
+ * 0 writes for the cycle edge so the parent's existing writes still
671
+ * dominate. Depth cap is `min(recipe.maxDepth ?? 3, F07_MAX_NESTED_DEPTH)`.
672
+ */
673
+ async function detectNestedRecipeWritesInPlan(steps, parentRecipePath, recipeMaxDepth, visited = new Set(), depth = 0) {
674
+ const cap = Math.min(recipeMaxDepth, F07_MAX_NESTED_DEPTH);
675
+ if (depth >= cap)
676
+ return;
677
+ for (const step of steps) {
678
+ if (step.type !== "recipe" || !step.recipe)
679
+ continue;
680
+ const nestedPath = resolveNestedRecipeForDryPlan(step.recipe, parentRecipePath);
681
+ if (!nestedPath)
682
+ continue;
683
+ if (visited.has(nestedPath)) {
684
+ // Cycle — count as 0 writes for this edge to break the loop.
685
+ step.nestedWriteCount = 0;
686
+ continue;
687
+ }
688
+ let nestedRecipe;
689
+ try {
690
+ nestedRecipe = loadYamlRecipe(nestedPath);
691
+ }
692
+ catch {
693
+ // Unparseable / unreadable nested recipe → treat as unknown writes
694
+ // (omit nestedWriteCount). Don't fail the whole dry-plan over a
695
+ // single broken sub-recipe.
696
+ continue;
697
+ }
698
+ const nextVisited = new Set(visited);
699
+ nextVisited.add(nestedPath);
700
+ // Build dry-run steps for the nested recipe and recurse.
701
+ const nestedTriggerType = nestedRecipe.trigger?.type;
702
+ let nestedSteps;
703
+ let nestedMaxDepth = recipeMaxDepth;
704
+ if (nestedTriggerType === "chained") {
705
+ const { generateExecutionPlan } = await import("../recipes/chainedRunner.js");
706
+ const plan = generateExecutionPlan(nestedRecipe);
707
+ nestedMaxDepth = plan.maxDepth;
708
+ nestedSteps = plan.steps.map((s) => {
709
+ const base = { id: s.id, type: s.type };
710
+ if (s.tool !== undefined)
711
+ base.tool = s.tool;
712
+ if (s.into !== undefined)
713
+ base.into = s.into;
714
+ if (s.recipe !== undefined)
715
+ base.recipe = s.recipe;
716
+ if (s.optional !== undefined)
717
+ base.optional = s.optional;
718
+ if (s.dependencies)
719
+ base.dependencies = s.dependencies;
720
+ if (s.condition !== undefined)
721
+ base.condition = s.condition;
722
+ if (s.risk !== undefined)
723
+ base.risk = s.risk;
724
+ return enrichStepFromRegistry(base);
725
+ });
726
+ }
727
+ else {
728
+ nestedSteps = buildSimpleRecipeDryRunSteps(nestedRecipe, {}).map(enrichStepFromRegistry);
729
+ }
730
+ // Recurse before counting so deeper nested writes propagate up.
731
+ await detectNestedRecipeWritesInPlan(nestedSteps, nestedPath, nestedMaxDepth, nextVisited, depth + 1);
732
+ let count = 0;
733
+ for (const ns of nestedSteps) {
734
+ if (ns.isWrite)
735
+ count += 1;
736
+ if (ns.nestedWriteCount && ns.nestedWriteCount > 0) {
737
+ count += ns.nestedWriteCount;
738
+ }
739
+ }
740
+ step.nestedWriteCount = count;
741
+ }
742
+ }
743
+ export async function runRecipeDryPlan(recipeRef, options = {}) {
744
+ const recipePath = resolveRecipePath(recipeRef);
745
+ const recipe = loadYamlRecipe(recipePath);
746
+ const triggerType = recipe.trigger?.type;
747
+ const selection = options.step
748
+ ? selectRecipeStep(recipe, options.step)
749
+ : undefined;
750
+ const recipeToPlan = selection
751
+ ? { ...recipe, steps: [selection.step] }
752
+ : recipe;
753
+ const generatedAt = new Date().toISOString();
754
+ // Collect lint results for the plan
755
+ const lintResult = runLint(recipePath);
756
+ const lint = {
757
+ errors: lintResult.issues
758
+ .filter((issue) => issue.level === "error")
759
+ .map((issue) => issue.message),
760
+ warnings: lintResult.issues
761
+ .filter((issue) => issue.level === "warning")
762
+ .map((issue) => issue.message),
763
+ };
764
+ if (triggerType === "chained") {
765
+ const { generateExecutionPlan } = await import("../recipes/chainedRunner.js");
766
+ const plan = generateExecutionPlan(recipeToPlan);
767
+ const steps = plan.steps.map((step) => {
768
+ const base = { id: step.id, type: step.type };
769
+ if (step.optional !== undefined)
770
+ base.optional = step.optional;
771
+ if (step.dependencies)
772
+ base.dependencies = step.dependencies;
773
+ if (step.condition !== undefined)
774
+ base.condition = step.condition;
775
+ if (step.risk !== undefined)
776
+ base.risk = step.risk;
777
+ // F-07 fix: read tool/into/recipe directly from the now-typed plan
778
+ // step instead of casting through `unknown`.
779
+ if (step.tool !== undefined)
780
+ base.tool = step.tool;
781
+ if (step.into !== undefined)
782
+ base.into = step.into;
783
+ if (step.recipe !== undefined)
784
+ base.recipe = step.recipe;
785
+ return enrichStepFromRegistry(base);
786
+ });
787
+ // F-07 fix: recurse into nested-recipe steps to detect writes the
788
+ // top-level enrichment cannot see. Without this, any chained recipe
789
+ // whose only writes live in a sub-recipe reports `hasWriteSteps:false`.
790
+ await detectNestedRecipeWritesInPlan(steps, recipePath, plan.maxDepth);
791
+ return {
792
+ schemaVersion: DRY_RUN_PLAN_SCHEMA_VERSION,
793
+ generatedAt,
794
+ recipe: recipe.name,
795
+ mode: "dry-run",
796
+ triggerType,
797
+ ...(selection ? { stepSelection: toStepSelection(selection) } : {}),
798
+ steps,
799
+ parallelGroups: plan.parallelGroups,
800
+ maxDepth: plan.maxDepth,
801
+ ...summarizePlanSteps(steps),
802
+ lint,
803
+ };
804
+ }
805
+ const steps = buildSimpleRecipeDryRunSteps(recipeToPlan, options.vars ?? {}).map(enrichStepFromRegistry);
806
+ return {
807
+ schemaVersion: DRY_RUN_PLAN_SCHEMA_VERSION,
808
+ generatedAt,
809
+ recipe: recipe.name,
810
+ mode: "dry-run",
811
+ triggerType: typeof triggerType === "string" ? triggerType : "manual",
812
+ ...(selection ? { stepSelection: toStepSelection(selection) } : {}),
813
+ steps,
814
+ ...summarizePlanSteps(steps),
815
+ lint,
816
+ };
817
+ }
818
+ /**
819
+ * Static policy check over a recipe: lint + dry-plan + unresolved/write/fixture checks.
820
+ * No connector calls, no agent calls — safe to run in CI.
821
+ */
822
+ export async function runPreflight(recipeRef, options = {}) {
823
+ const recipePath = resolveRecipePath(recipeRef);
824
+ const issues = [];
825
+ const lint = runLint(recipePath);
826
+ for (const issue of lint.issues) {
827
+ issues.push({
828
+ level: issue.level,
829
+ code: issue.level === "error" ? "lint-error" : "lint-warning",
830
+ message: issue.message,
831
+ });
832
+ }
833
+ const plan = await runRecipeDryPlan(recipeRef, options);
834
+ const requireWriteAck = options.requireWriteAck ?? true;
835
+ const allowlist = new Set(options.allowWrites ?? []);
836
+ for (const step of plan.steps) {
837
+ if (step.type === "tool" && step.tool && step.resolved === false) {
838
+ issues.push({
839
+ level: "error",
840
+ code: "unresolved-tool",
841
+ message: `Tool "${step.tool}" is not registered`,
842
+ stepId: step.id,
843
+ tool: step.tool,
844
+ ...(step.namespace ? { namespace: step.namespace } : {}),
845
+ });
846
+ }
847
+ if (requireWriteAck &&
848
+ step.isWrite === true &&
849
+ step.tool &&
850
+ !allowlist.has(step.tool) &&
851
+ !(step.namespace && allowlist.has(step.namespace))) {
852
+ issues.push({
853
+ level: "error",
854
+ code: "unacknowledged-write",
855
+ message: `Step "${step.id}" performs a write via "${step.tool}" but is not acknowledged via allowWrites`,
856
+ stepId: step.id,
857
+ tool: step.tool,
858
+ ...(step.namespace ? { namespace: step.namespace } : {}),
859
+ });
860
+ }
861
+ }
862
+ if (options.requireFixtures && plan.connectorNamespaces) {
863
+ const fixturesDir = options.fixturesDir ?? FIXTURES_DIR;
864
+ for (const ns of plan.connectorNamespaces) {
865
+ const library = loadFixtureLibrary(join(fixturesDir, `${ns}.json`));
866
+ if (!library) {
867
+ issues.push({
868
+ level: "error",
869
+ code: "missing-fixture",
870
+ message: `Missing fixture library for connector "${ns}" at ${fixturesDir}/${ns}.json`,
871
+ namespace: ns,
872
+ });
873
+ }
874
+ }
875
+ }
876
+ const ok = !issues.some((issue) => issue.level === "error");
877
+ return { ok, recipe: plan.recipe, issues, plan };
878
+ }
879
+ /**
880
+ * Watch a recipe file and run preflight on every save (debounced). Composes
881
+ * runWatch + runPreflight so editor integrations get live policy feedback
882
+ * without spawning the CLI per keystroke.
883
+ *
884
+ * Returns a stop function. If a preflight is in-flight when a new save lands,
885
+ * at most one rerun is queued (matches runWatch semantics).
886
+ */
887
+ export function runPreflightWatch(options) {
888
+ const { recipePath, onResult, onError, debounceMs, watchFactory, ...preflightOptions } = options;
889
+ const watchOptions = {
890
+ recipePath,
891
+ onChange: async () => {
892
+ const result = await runPreflight(recipePath, preflightOptions);
893
+ await onResult(result);
894
+ },
895
+ ...(onError ? { onError } : {}),
896
+ ...(debounceMs !== undefined ? { debounceMs } : {}),
897
+ ...(watchFactory ? { watchFactory } : {}),
898
+ };
899
+ return runWatch(watchOptions);
900
+ }
901
+ /**
902
+ * Exported for the cli-warns-when-out-of-jail.test regression suite —
903
+ * the warn path is what we assert on, not the path-resolution behaviour.
904
+ * Internal callers should keep using the local symbol.
905
+ */
906
+ export function resolveRecipeRefForCli(recipeRef) {
907
+ return resolveRecipePath(recipeRef);
908
+ }
909
+ function resolveRecipePath(recipeRef) {
910
+ const directPath = resolve(recipeRef);
911
+ if (existsSync(directPath) && statSync(directPath).isFile()) {
912
+ // F-10 CLI warn: a recipe file outside the recipe jail (e.g. a YAML
913
+ // dragged in from /tmp) still loads — but emit a stderr notice so the
914
+ // user knows they're loading a recipe whose tool dispatches will hit
915
+ // the jail check at runtime. The jail roots default to ~/.patchwork +
916
+ // CWD; /tmp is opt-in via CLAUDE_IDE_BRIDGE_RECIPE_TMP_JAIL=1.
917
+ if (tryResolveRecipePath(directPath) === null) {
918
+ console.warn(`warning: recipe file "${directPath}" lives outside the recipe jail (~/.patchwork, workspace). Tool dispatches that touch the filesystem will be rejected unless the path is inside the jail. Set CLAUDE_IDE_BRIDGE_RECIPE_TMP_JAIL=1 to opt /tmp into the jail.`);
919
+ }
920
+ return directPath;
921
+ }
922
+ const bundledDir = fileURLToPath(new URL("../../templates/recipes", import.meta.url));
923
+ const normalizedRef = recipeRef.replace(/\.(yaml|yml|json)$/i, "");
924
+ const candidates = [
925
+ join(RECIPES_DIR, `${normalizedRef}.yaml`),
926
+ join(RECIPES_DIR, `${normalizedRef}.yml`),
927
+ join(RECIPES_DIR, `${normalizedRef}.json`),
928
+ join(bundledDir, `${normalizedRef}.yaml`),
929
+ join(bundledDir, `${normalizedRef}.yml`),
930
+ ];
931
+ for (const candidate of candidates) {
932
+ if (existsSync(candidate) && statSync(candidate).isFile()) {
933
+ return candidate;
934
+ }
935
+ }
936
+ // Install-dir resolution (DB-4): `recipe install` puts each recipe in
937
+ // its own subdir at `<RECIPES_DIR>/<install-name>/<entrypoint>.yaml`.
938
+ // Without this fallback, `recipe run <install-name>` errors with
939
+ // "not found" even though `recipe list` displays the recipe — observed
940
+ // in the 2026-04-29 dogfood pass.
941
+ try {
942
+ const entrypoint = findInstalledRecipeEntrypoint(normalizedRef);
943
+ if (entrypoint)
944
+ return entrypoint;
945
+ }
946
+ catch {
947
+ // findInstalledRecipeEntrypoint throws on path-traversal `name`
948
+ // values via its underlying validator. That's a security boundary,
949
+ // not a UX one — fall through to the standard "not found" error
950
+ // rather than leaking the validator message to a normal user typo.
951
+ }
952
+ throw new Error(`recipe "${basename(recipeRef)}" not found in ${RECIPES_DIR}`);
953
+ }
954
+ function selectRecipeStep(recipe, query) {
955
+ const matches = recipe.steps
956
+ .map((step) => {
957
+ const match = matchRecipeStep(step, query);
958
+ return match ? { ...match, query, step } : undefined;
959
+ })
960
+ .filter((match) => Boolean(match));
961
+ if (matches.length === 0) {
962
+ throw new Error(`Step "${query}" not found in recipe "${recipe.name}"`);
963
+ }
964
+ if (matches.length > 1) {
965
+ const labels = matches
966
+ .map((match) => `${match.matchedBy}:${match.matchedValue}`)
967
+ .join(", ");
968
+ throw new Error(`Step "${query}" is ambiguous in recipe "${recipe.name}": ${labels}`);
969
+ }
970
+ const [match] = matches;
971
+ if (!match) {
972
+ throw new Error(`Step "${query}" not found in recipe "${recipe.name}"`);
973
+ }
974
+ return match;
975
+ }
976
+ function matchRecipeStep(step, query) {
977
+ const id = typeof step.id === "string" ? step.id : undefined;
978
+ if (id === query) {
979
+ return { matchedBy: "id", matchedValue: id };
980
+ }
981
+ const into = getStepInto(step);
982
+ if (into === query) {
983
+ return { matchedBy: "into", matchedValue: into };
984
+ }
985
+ const tool = typeof step.tool === "string" ? step.tool : undefined;
986
+ if (tool === query) {
987
+ return { matchedBy: "tool", matchedValue: tool };
988
+ }
989
+ return null;
990
+ }
991
+ function getStepInto(step) {
992
+ if (typeof step.into === "string" && step.into) {
993
+ return step.into;
994
+ }
995
+ if (step.agent &&
996
+ typeof step.agent === "object" &&
997
+ typeof step.agent.into === "string" &&
998
+ step.agent.into) {
999
+ return step.agent.into;
1000
+ }
1001
+ return undefined;
1002
+ }
1003
+ function toStepSelection(selection) {
1004
+ return {
1005
+ query: selection.query,
1006
+ matchedBy: selection.matchedBy,
1007
+ matchedValue: selection.matchedValue,
1008
+ };
1009
+ }
1010
+ function buildSimpleRecipeDryRunSteps(recipe, vars) {
1011
+ const now = new Date();
1012
+ const ctx = {
1013
+ date: now.toISOString().slice(0, 10),
1014
+ time: now.toTimeString().slice(0, 5),
1015
+ ...vars,
1016
+ };
1017
+ return recipe.steps.map((step, index) => {
1018
+ const id = (typeof step.id === "string" && step.id) ||
1019
+ getStepInto(step) ||
1020
+ step.tool ||
1021
+ `step_${index}`;
1022
+ if (step.agent) {
1023
+ const prompt = render(step.agent.prompt, ctx);
1024
+ const into = getStepInto(step);
1025
+ if (into) {
1026
+ ctx[into] = `[dry-run:${id}]`;
1027
+ }
1028
+ return {
1029
+ id,
1030
+ type: "agent",
1031
+ into,
1032
+ optional: step.optional,
1033
+ prompt,
1034
+ };
1035
+ }
1036
+ const params = {};
1037
+ for (const [key, value] of Object.entries(step)) {
1038
+ if (key === "tool" || key === "agent" || key === "into" || key === "id") {
1039
+ continue;
1040
+ }
1041
+ params[key] = typeof value === "string" ? render(value, ctx) : value;
1042
+ }
1043
+ const into = getStepInto(step);
1044
+ if (into) {
1045
+ ctx[into] = `[dry-run:${id}]`;
1046
+ if (step.tool) {
1047
+ seedToolOutputPreviewContext(step.tool, into, id, ctx);
1048
+ }
1049
+ }
1050
+ return {
1051
+ id,
1052
+ type: "tool",
1053
+ tool: step.tool,
1054
+ into,
1055
+ optional: step.optional,
1056
+ params,
1057
+ };
1058
+ });
1059
+ }
1060
+ export async function runRecord(recipePath, options = {}) {
1061
+ const lint = runLint(recipePath);
1062
+ const issues = [...lint.issues];
1063
+ const fixturesDir = options.fixturesDir ?? FIXTURES_DIR;
1064
+ let recordedFixtures = [];
1065
+ let stepsRun = 0;
1066
+ let outputs = [];
1067
+ if (issues.every((issue) => issue.level !== "error")) {
1068
+ try {
1069
+ const recipe = loadYamlRecipe(recipePath);
1070
+ recordedFixtures = getRequiredFixtureNamespaces(recipe.steps);
1071
+ const run = await runYamlRecipe(recipe, {
1072
+ ...options.deps,
1073
+ recordFixturesDir: fixturesDir,
1074
+ });
1075
+ stepsRun = run.stepsRun;
1076
+ outputs = run.outputs;
1077
+ if (run.errorMessage) {
1078
+ issues.push({
1079
+ level: "error",
1080
+ message: run.errorMessage,
1081
+ });
1082
+ }
1083
+ }
1084
+ catch (err) {
1085
+ issues.push({
1086
+ level: "error",
1087
+ message: err instanceof Error ? err.message : String(err),
1088
+ });
1089
+ }
1090
+ }
1091
+ const errors = issues.filter((issue) => issue.level === "error").length;
1092
+ const warnings = issues.filter((issue) => issue.level === "warning").length;
1093
+ return {
1094
+ valid: errors === 0,
1095
+ issues,
1096
+ warnings,
1097
+ errors,
1098
+ recordedFixtures,
1099
+ stepsRun,
1100
+ outputs,
1101
+ };
1102
+ }
1103
+ export async function runTest(recipePath, options = {}) {
1104
+ const lint = runLint(recipePath);
1105
+ const fixturesDir = options.fixturesDir ?? FIXTURES_DIR;
1106
+ const issues = [...lint.issues];
1107
+ let requiredFixtures = [];
1108
+ let stepsRun = 0;
1109
+ let outputs = [];
1110
+ let assertionFailures = [];
1111
+ if (existsSync(recipePath)) {
1112
+ try {
1113
+ const recipe = parseYaml(readFileSync(recipePath, "utf-8"));
1114
+ requiredFixtures = getRequiredFixtureNamespaces(recipe.steps ?? []);
1115
+ }
1116
+ catch {
1117
+ requiredFixtures = [];
1118
+ }
1119
+ }
1120
+ const missingFixtures = requiredFixtures.filter((provider) => !existsSync(join(fixturesDir, `${provider}.json`)));
1121
+ for (const provider of missingFixtures) {
1122
+ issues.push({
1123
+ level: "error",
1124
+ message: `Missing fixture library for connector '${provider}' at ${join(fixturesDir, `${provider}.json`)}`,
1125
+ });
1126
+ }
1127
+ if (issues.every((issue) => issue.level !== "error")) {
1128
+ try {
1129
+ const recipe = loadYamlRecipe(recipePath);
1130
+ const triggerType = recipe.trigger?.type;
1131
+ if (triggerType === "chained") {
1132
+ // Chained recipes: run through chainedRunner with mocked tool + agent executors
1133
+ const { runChainedRecipe } = await import("../recipes/chainedRunner.js");
1134
+ const { evaluateExpect } = await import("../recipes/yamlRunner.js");
1135
+ const chainedRecipe = recipe;
1136
+ const recipeRecord = recipe;
1137
+ const run = await runChainedRecipe(chainedRecipe, {
1138
+ env: process.env,
1139
+ maxConcurrency: recipeRecord.maxConcurrency ?? 4,
1140
+ maxDepth: recipeRecord.maxDepth ?? 3,
1141
+ dryRun: false,
1142
+ sourcePath: recipePath,
1143
+ }, {
1144
+ executeTool: async (tool) => `[mock:${tool}]`,
1145
+ executeAgent: async () => "[mock agent output]",
1146
+ loadNestedRecipe: async () => null,
1147
+ });
1148
+ stepsRun = run.summary.total;
1149
+ if (run.errorMessage) {
1150
+ issues.push({ level: "error", message: run.errorMessage });
1151
+ }
1152
+ // Evaluate expect: block against chained run results
1153
+ const expectBlock = recipeRecord.expect;
1154
+ if (expectBlock) {
1155
+ const failures = evaluateExpect({
1156
+ stepsRun: run.summary.total,
1157
+ outputs: [],
1158
+ context: run.context,
1159
+ errorMessage: run.errorMessage,
1160
+ }, expectBlock);
1161
+ assertionFailures = failures;
1162
+ for (const failure of failures) {
1163
+ issues.push({ level: "error", message: failure.message });
1164
+ }
1165
+ }
1166
+ }
1167
+ else {
1168
+ const mockConnectors = createMockToolConnectors(recipe.steps, fixturesDir);
1169
+ const run = await runYamlRecipe(recipe, {
1170
+ testMode: true,
1171
+ mockConnectors,
1172
+ readFile: (filePath) => readFileSync(filePath, "utf-8"),
1173
+ writeFile: () => { },
1174
+ appendFile: () => { },
1175
+ mkdir: () => { },
1176
+ gitLogSince: () => "[mock git log]",
1177
+ gitStaleBranches: () => "[mock stale branches]",
1178
+ getDiagnostics: () => "[mock diagnostics]",
1179
+ claudeFn: async () => "[mock agent output]",
1180
+ claudeCodeFn: async () => "[mock agent output]",
1181
+ providerDriverFn: async () => "[mock agent output]",
1182
+ });
1183
+ stepsRun = run.stepsRun;
1184
+ outputs = run.outputs;
1185
+ if (run.assertionFailures && run.assertionFailures.length > 0) {
1186
+ assertionFailures = run.assertionFailures;
1187
+ for (const failure of run.assertionFailures) {
1188
+ issues.push({ level: "error", message: failure.message });
1189
+ }
1190
+ }
1191
+ if (run.errorMessage) {
1192
+ issues.push({ level: "error", message: run.errorMessage });
1193
+ }
1194
+ }
1195
+ }
1196
+ catch (err) {
1197
+ issues.push({
1198
+ level: "error",
1199
+ message: err instanceof Error ? err.message : String(err),
1200
+ });
1201
+ }
1202
+ }
1203
+ const errors = issues.filter((issue) => issue.level === "error").length;
1204
+ const warnings = issues.filter((issue) => issue.level === "warning").length;
1205
+ return {
1206
+ valid: errors === 0,
1207
+ issues,
1208
+ warnings,
1209
+ errors,
1210
+ requiredFixtures,
1211
+ missingFixtures,
1212
+ stepsRun,
1213
+ outputs,
1214
+ assertionFailures,
1215
+ };
1216
+ }
1217
+ /**
1218
+ * Watch a recipe file and re-run `patchwork recipe test` on every save (debounced).
1219
+ * Mirrors runPreflightWatch — composes runWatch + runTest.
1220
+ * Returns a stop function.
1221
+ */
1222
+ export function runTestWatch(options) {
1223
+ const { recipePath, fixturesDir, onResult, onError, debounceMs, watchFactory, } = options;
1224
+ return runWatch({
1225
+ recipePath,
1226
+ onChange: async () => {
1227
+ const result = await runTest(recipePath, { fixturesDir });
1228
+ await onResult(result);
1229
+ },
1230
+ ...(onError ? { onError } : {}),
1231
+ ...(debounceMs !== undefined ? { debounceMs } : {}),
1232
+ ...(watchFactory ? { watchFactory } : {}),
1233
+ });
1234
+ }
1235
+ function getRequiredFixtureNamespaces(steps) {
1236
+ const namespaces = new Set();
1237
+ for (const step of steps) {
1238
+ const tool = step.tool;
1239
+ if (typeof tool !== "string") {
1240
+ continue;
1241
+ }
1242
+ const namespace = tool.split(".")[0];
1243
+ if (namespace && isConnectorNamespace(namespace)) {
1244
+ namespaces.add(namespace);
1245
+ }
1246
+ }
1247
+ return [...namespaces].sort();
1248
+ }
1249
+ function createMockToolConnectors(steps, fixturesDir) {
1250
+ const providerConnectors = new Map();
1251
+ const toolConnectors = {};
1252
+ for (const step of steps) {
1253
+ const tool = step.tool;
1254
+ if (typeof tool !== "string") {
1255
+ continue;
1256
+ }
1257
+ const [namespace, operation] = tool.split(".");
1258
+ if (!namespace || !operation || !isConnectorNamespace(namespace)) {
1259
+ continue;
1260
+ }
1261
+ let connector = providerConnectors.get(namespace);
1262
+ if (!connector) {
1263
+ connector = new MockConnector(namespace, {
1264
+ fixturePath: join(fixturesDir, `${namespace}.json`),
1265
+ });
1266
+ providerConnectors.set(namespace, connector);
1267
+ }
1268
+ toolConnectors[tool] = {
1269
+ invoke: async (_unusedOperation, input) => {
1270
+ const output = await connector.invoke(operation, input);
1271
+ return (typeof output === "string" ? output : JSON.stringify(output));
1272
+ },
1273
+ };
1274
+ }
1275
+ return toolConnectors;
1276
+ }
1277
+ function normalizeChangedFile(changedFile) {
1278
+ if (typeof changedFile === "string") {
1279
+ return changedFile;
1280
+ }
1281
+ if (changedFile instanceof Buffer) {
1282
+ return changedFile.toString();
1283
+ }
1284
+ return null;
1285
+ }
1286
+ export function runWatch(options) {
1287
+ const dir = dirname(resolve(options.recipePath));
1288
+ const filename = basename(options.recipePath);
1289
+ const debounceMs = options.debounceMs ?? 300;
1290
+ const watchFactory = options.watchFactory ??
1291
+ ((watchPath, watchOptions, listener) => watch(watchPath, watchOptions, listener));
1292
+ let debounceTimer = null;
1293
+ let running = false;
1294
+ let rerunQueued = false;
1295
+ let stopped = false;
1296
+ const handleError = (err) => {
1297
+ options.onError?.(err instanceof Error ? err : new Error(String(err)));
1298
+ };
1299
+ const finishChange = () => {
1300
+ running = false;
1301
+ if (stopped || !rerunQueued) {
1302
+ return;
1303
+ }
1304
+ rerunQueued = false;
1305
+ executeChange();
1306
+ };
1307
+ const executeChange = () => {
1308
+ if (stopped) {
1309
+ return;
1310
+ }
1311
+ if (running) {
1312
+ rerunQueued = true;
1313
+ return;
1314
+ }
1315
+ running = true;
1316
+ try {
1317
+ const changeResult = options.onChange();
1318
+ void Promise.resolve(changeResult)
1319
+ .catch(handleError)
1320
+ .finally(finishChange);
1321
+ }
1322
+ catch (err) {
1323
+ handleError(err);
1324
+ finishChange();
1325
+ }
1326
+ };
1327
+ const scheduleChange = () => {
1328
+ if (stopped) {
1329
+ return;
1330
+ }
1331
+ if (running) {
1332
+ rerunQueued = true;
1333
+ return;
1334
+ }
1335
+ if (debounceTimer) {
1336
+ clearTimeout(debounceTimer);
1337
+ }
1338
+ debounceTimer = setTimeout(() => {
1339
+ debounceTimer = null;
1340
+ executeChange();
1341
+ }, debounceMs);
1342
+ };
1343
+ const watcher = watchFactory(dir, { recursive: false }, (_eventType, changedFile) => {
1344
+ const changedName = normalizeChangedFile(changedFile);
1345
+ if (changedName === filename) {
1346
+ scheduleChange();
1347
+ }
1348
+ });
1349
+ // Return cleanup function
1350
+ return () => {
1351
+ stopped = true;
1352
+ if (debounceTimer) {
1353
+ clearTimeout(debounceTimer);
1354
+ debounceTimer = null;
1355
+ }
1356
+ watcher.close();
1357
+ };
1358
+ }
1359
+ //# sourceMappingURL=recipe.js.map