patchwork-os 0.2.0-beta.1 → 0.2.0-beta.10.canary.100

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 (701) hide show
  1. package/README.bridge.md +14 -14
  2. package/README.md +201 -34
  3. package/deploy/README.md +1 -1
  4. package/deploy/bootstrap-vps.sh +14 -9
  5. package/deploy/deploy-dashboard.sh +25 -1
  6. package/deploy/macos/README.md +153 -0
  7. package/deploy/macos/com.patchwork.bridge.plist.template +54 -0
  8. package/deploy/macos/com.patchwork.tunnel.plist.template +76 -0
  9. package/deploy/macos/install-mac-bridge.sh +244 -0
  10. package/deploy/macos/uninstall-mac-bridge.sh +22 -0
  11. package/dist/activityLog.d.ts +6 -0
  12. package/dist/activityLog.js +63 -26
  13. package/dist/activityLog.js.map +1 -1
  14. package/dist/adapters/claude.js +22 -16
  15. package/dist/adapters/claude.js.map +1 -1
  16. package/dist/adapters/gemini.js +15 -11
  17. package/dist/adapters/gemini.js.map +1 -1
  18. package/dist/adapters/openai.js +9 -9
  19. package/dist/adapters/openai.js.map +1 -1
  20. package/dist/ajv2020.d.ts +25 -0
  21. package/dist/ajv2020.js +33 -0
  22. package/dist/ajv2020.js.map +1 -0
  23. package/dist/analyticsAggregator.js +1 -1
  24. package/dist/analyticsAggregator.js.map +1 -1
  25. package/dist/analyticsConfig.d.ts +29 -0
  26. package/dist/analyticsConfig.js +102 -0
  27. package/dist/analyticsConfig.js.map +1 -0
  28. package/dist/analyticsPrefs.d.ts +38 -2
  29. package/dist/analyticsPrefs.js +148 -23
  30. package/dist/analyticsPrefs.js.map +1 -1
  31. package/dist/analyticsSend.d.ts +17 -1
  32. package/dist/analyticsSend.js +67 -5
  33. package/dist/analyticsSend.js.map +1 -1
  34. package/dist/approvalHttp.d.ts +14 -0
  35. package/dist/approvalHttp.js +212 -9
  36. package/dist/approvalHttp.js.map +1 -1
  37. package/dist/approvalInsights.js +13 -5
  38. package/dist/approvalInsights.js.map +1 -1
  39. package/dist/approvalQueue.d.ts +97 -3
  40. package/dist/approvalQueue.js +193 -19
  41. package/dist/approvalQueue.js.map +1 -1
  42. package/dist/approvalSignals.js +19 -11
  43. package/dist/approvalSignals.js.map +1 -1
  44. package/dist/automation.d.ts +35 -10
  45. package/dist/automation.js +133 -37
  46. package/dist/automation.js.map +1 -1
  47. package/dist/automationSuggestions.js +10 -8
  48. package/dist/automationSuggestions.js.map +1 -1
  49. package/dist/bridge.d.ts +2 -0
  50. package/dist/bridge.js +219 -35
  51. package/dist/bridge.js.map +1 -1
  52. package/dist/bridgeLockDiscovery.d.ts +27 -1
  53. package/dist/bridgeLockDiscovery.js +38 -11
  54. package/dist/bridgeLockDiscovery.js.map +1 -1
  55. package/dist/bridgeToken.js +3 -2
  56. package/dist/bridgeToken.js.map +1 -1
  57. package/dist/claudeDriver.js +23 -8
  58. package/dist/claudeDriver.js.map +1 -1
  59. package/dist/claudeOrchestrator.d.ts +1 -1
  60. package/dist/claudeOrchestrator.js +87 -44
  61. package/dist/claudeOrchestrator.js.map +1 -1
  62. package/dist/commands/analytics.d.ts +8 -0
  63. package/dist/commands/analytics.js +134 -0
  64. package/dist/commands/analytics.js.map +1 -0
  65. package/dist/commands/auditEnv.d.ts +36 -0
  66. package/dist/commands/auditEnv.js +202 -0
  67. package/dist/commands/auditEnv.js.map +1 -0
  68. package/dist/commands/connect.d.ts +47 -0
  69. package/dist/commands/connect.js +419 -0
  70. package/dist/commands/connect.js.map +1 -0
  71. package/dist/commands/dashboard.js +8 -1
  72. package/dist/commands/dashboard.js.map +1 -1
  73. package/dist/commands/doctor.d.ts +35 -0
  74. package/dist/commands/doctor.js +51 -0
  75. package/dist/commands/doctor.js.map +1 -0
  76. package/dist/commands/install.js +3 -0
  77. package/dist/commands/install.js.map +1 -1
  78. package/dist/commands/launchd.js +15 -16
  79. package/dist/commands/launchd.js.map +1 -1
  80. package/dist/commands/patchworkInit.d.ts +5 -0
  81. package/dist/commands/patchworkInit.js +89 -7
  82. package/dist/commands/patchworkInit.js.map +1 -1
  83. package/dist/commands/recipe.d.ts +151 -0
  84. package/dist/commands/recipe.js +649 -9
  85. package/dist/commands/recipe.js.map +1 -1
  86. package/dist/commands/recipeInstall.js +11 -4
  87. package/dist/commands/recipeInstall.js.map +1 -1
  88. package/dist/commands/shadowScan.d.ts +34 -0
  89. package/dist/commands/shadowScan.js +142 -0
  90. package/dist/commands/shadowScan.js.map +1 -0
  91. package/dist/commands/task.d.ts +25 -0
  92. package/dist/commands/task.js +62 -42
  93. package/dist/commands/task.js.map +1 -1
  94. package/dist/commands/tools.d.ts +20 -1
  95. package/dist/commands/tools.js +112 -3
  96. package/dist/commands/tools.js.map +1 -1
  97. package/dist/commands/tracesExport.d.ts +15 -1
  98. package/dist/commands/tracesExport.js +39 -5
  99. package/dist/commands/tracesExport.js.map +1 -1
  100. package/dist/commands/tracesImport.js +21 -4
  101. package/dist/commands/tracesImport.js.map +1 -1
  102. package/dist/commitIssueLinkLog.d.ts +16 -0
  103. package/dist/commitIssueLinkLog.js +110 -24
  104. package/dist/commitIssueLinkLog.js.map +1 -1
  105. package/dist/companions/registry.js +15 -7
  106. package/dist/companions/registry.js.map +1 -1
  107. package/dist/config.d.ts +47 -3
  108. package/dist/config.js +111 -21
  109. package/dist/config.js.map +1 -1
  110. package/dist/connectorRoutes.js +1422 -57
  111. package/dist/connectorRoutes.js.map +1 -1
  112. package/dist/connectors/airtable.d.ts +230 -0
  113. package/dist/connectors/airtable.js +700 -0
  114. package/dist/connectors/airtable.js.map +1 -0
  115. package/dist/connectors/asana.js +13 -9
  116. package/dist/connectors/asana.js.map +1 -1
  117. package/dist/connectors/baseConnector.js +25 -3
  118. package/dist/connectors/baseConnector.js.map +1 -1
  119. package/dist/connectors/caldiy.d.ts +137 -0
  120. package/dist/connectors/caldiy.js +424 -0
  121. package/dist/connectors/caldiy.js.map +1 -0
  122. package/dist/connectors/circleci.d.ts +162 -0
  123. package/dist/connectors/circleci.js +576 -0
  124. package/dist/connectors/circleci.js.map +1 -0
  125. package/dist/connectors/cloudflare.d.ts +132 -0
  126. package/dist/connectors/cloudflare.js +622 -0
  127. package/dist/connectors/cloudflare.js.map +1 -0
  128. package/dist/connectors/confluence.js +35 -0
  129. package/dist/connectors/confluence.js.map +1 -1
  130. package/dist/connectors/connectorRedirectUri.d.ts +30 -0
  131. package/dist/connectors/connectorRedirectUri.js +38 -0
  132. package/dist/connectors/connectorRedirectUri.js.map +1 -0
  133. package/dist/connectors/connectorRegistry.d.ts +60 -0
  134. package/dist/connectors/connectorRegistry.js +354 -0
  135. package/dist/connectors/connectorRegistry.js.map +1 -0
  136. package/dist/connectors/datadog.js +41 -5
  137. package/dist/connectors/datadog.js.map +1 -1
  138. package/dist/connectors/discord.js +14 -10
  139. package/dist/connectors/discord.js.map +1 -1
  140. package/dist/connectors/elasticsearch.d.ts +141 -0
  141. package/dist/connectors/elasticsearch.js +601 -0
  142. package/dist/connectors/elasticsearch.js.map +1 -0
  143. package/dist/connectors/figma.d.ts +130 -0
  144. package/dist/connectors/figma.js +387 -0
  145. package/dist/connectors/figma.js.map +1 -0
  146. package/dist/connectors/github.d.ts +17 -0
  147. package/dist/connectors/github.js +53 -2
  148. package/dist/connectors/github.js.map +1 -1
  149. package/dist/connectors/gitlab.js +12 -6
  150. package/dist/connectors/gitlab.js.map +1 -1
  151. package/dist/connectors/gmail.js +72 -21
  152. package/dist/connectors/gmail.js.map +1 -1
  153. package/dist/connectors/googleCalendar.js +8 -8
  154. package/dist/connectors/googleCalendar.js.map +1 -1
  155. package/dist/connectors/googleDocs.d.ts +103 -0
  156. package/dist/connectors/googleDocs.js +409 -0
  157. package/dist/connectors/googleDocs.js.map +1 -0
  158. package/dist/connectors/googleDrive.d.ts +12 -0
  159. package/dist/connectors/googleDrive.js +35 -8
  160. package/dist/connectors/googleDrive.js.map +1 -1
  161. package/dist/connectors/grafana.d.ts +133 -0
  162. package/dist/connectors/grafana.js +478 -0
  163. package/dist/connectors/grafana.js.map +1 -0
  164. package/dist/connectors/jira.d.ts +25 -0
  165. package/dist/connectors/jira.js +234 -3
  166. package/dist/connectors/jira.js.map +1 -1
  167. package/dist/connectors/linear.js +1 -1
  168. package/dist/connectors/linear.js.map +1 -1
  169. package/dist/connectors/mcpClient.js +37 -23
  170. package/dist/connectors/mcpClient.js.map +1 -1
  171. package/dist/connectors/mcpOAuth.js +78 -16
  172. package/dist/connectors/mcpOAuth.js.map +1 -1
  173. package/dist/connectors/monday.d.ts +217 -0
  174. package/dist/connectors/monday.js +655 -0
  175. package/dist/connectors/monday.js.map +1 -0
  176. package/dist/connectors/mongodb.d.ts +139 -0
  177. package/dist/connectors/mongodb.js +455 -0
  178. package/dist/connectors/mongodb.js.map +1 -0
  179. package/dist/connectors/notion.d.ts +1 -1
  180. package/dist/connectors/notion.js +13 -4
  181. package/dist/connectors/notion.js.map +1 -1
  182. package/dist/connectors/oauthStateStore.d.ts +20 -0
  183. package/dist/connectors/oauthStateStore.js +94 -4
  184. package/dist/connectors/oauthStateStore.js.map +1 -1
  185. package/dist/connectors/obsidian.d.ts +83 -0
  186. package/dist/connectors/obsidian.js +441 -0
  187. package/dist/connectors/obsidian.js.map +1 -0
  188. package/dist/connectors/paystack.d.ts +159 -0
  189. package/dist/connectors/paystack.js +607 -0
  190. package/dist/connectors/paystack.js.map +1 -0
  191. package/dist/connectors/pipedrive.d.ts +189 -0
  192. package/dist/connectors/pipedrive.js +559 -0
  193. package/dist/connectors/pipedrive.js.map +1 -0
  194. package/dist/connectors/postgres.d.ts +127 -0
  195. package/dist/connectors/postgres.js +512 -0
  196. package/dist/connectors/postgres.js.map +1 -0
  197. package/dist/connectors/posthog.d.ts +119 -0
  198. package/dist/connectors/posthog.js +479 -0
  199. package/dist/connectors/posthog.js.map +1 -0
  200. package/dist/connectors/redis.d.ts +140 -0
  201. package/dist/connectors/redis.js +579 -0
  202. package/dist/connectors/redis.js.map +1 -0
  203. package/dist/connectors/resend.d.ts +131 -0
  204. package/dist/connectors/resend.js +529 -0
  205. package/dist/connectors/resend.js.map +1 -0
  206. package/dist/connectors/salesforce.d.ts +136 -0
  207. package/dist/connectors/salesforce.js +603 -0
  208. package/dist/connectors/salesforce.js.map +1 -0
  209. package/dist/connectors/secrets.d.ts +51 -0
  210. package/dist/connectors/secrets.js +102 -0
  211. package/dist/connectors/secrets.js.map +1 -0
  212. package/dist/connectors/sendgrid.d.ts +102 -0
  213. package/dist/connectors/sendgrid.js +423 -0
  214. package/dist/connectors/sendgrid.js.map +1 -0
  215. package/dist/connectors/shopify.d.ts +145 -0
  216. package/dist/connectors/shopify.js +502 -0
  217. package/dist/connectors/shopify.js.map +1 -0
  218. package/dist/connectors/slack.d.ts +1 -1
  219. package/dist/connectors/slack.js +61 -9
  220. package/dist/connectors/slack.js.map +1 -1
  221. package/dist/connectors/snowflake.d.ts +119 -0
  222. package/dist/connectors/snowflake.js +633 -0
  223. package/dist/connectors/snowflake.js.map +1 -0
  224. package/dist/connectors/supabase.d.ts +92 -0
  225. package/dist/connectors/supabase.js +630 -0
  226. package/dist/connectors/supabase.js.map +1 -0
  227. package/dist/connectors/todoist.d.ts +117 -0
  228. package/dist/connectors/todoist.js +485 -0
  229. package/dist/connectors/todoist.js.map +1 -0
  230. package/dist/connectors/tokenStorage.js +56 -14
  231. package/dist/connectors/tokenStorage.js.map +1 -1
  232. package/dist/connectors/twilio.d.ts +118 -0
  233. package/dist/connectors/twilio.js +475 -0
  234. package/dist/connectors/twilio.js.map +1 -0
  235. package/dist/connectors/vercel.d.ts +110 -0
  236. package/dist/connectors/vercel.js +479 -0
  237. package/dist/connectors/vercel.js.map +1 -0
  238. package/dist/connectors/webflow.d.ts +118 -0
  239. package/dist/connectors/webflow.js +393 -0
  240. package/dist/connectors/webflow.js.map +1 -0
  241. package/dist/connectors/woocommerce.d.ts +220 -0
  242. package/dist/connectors/woocommerce.js +464 -0
  243. package/dist/connectors/woocommerce.js.map +1 -0
  244. package/dist/crypto.js +7 -2
  245. package/dist/crypto.js.map +1 -1
  246. package/dist/decisionReplay.d.ts +8 -6
  247. package/dist/decisionReplay.js +50 -17
  248. package/dist/decisionReplay.js.map +1 -1
  249. package/dist/decisionTraceLog.d.ts +28 -0
  250. package/dist/decisionTraceLog.js +156 -29
  251. package/dist/decisionTraceLog.js.map +1 -1
  252. package/dist/drivers/claude/api.js +33 -9
  253. package/dist/drivers/claude/api.js.map +1 -1
  254. package/dist/drivers/claude/envSanitizer.d.ts +0 -6
  255. package/dist/drivers/claude/envSanitizer.js +17 -2
  256. package/dist/drivers/claude/envSanitizer.js.map +1 -1
  257. package/dist/drivers/claude/streamParser.js +5 -4
  258. package/dist/drivers/claude/streamParser.js.map +1 -1
  259. package/dist/drivers/claude/subprocess.js +69 -16
  260. package/dist/drivers/claude/subprocess.js.map +1 -1
  261. package/dist/drivers/gemini/index.d.ts +22 -0
  262. package/dist/drivers/gemini/index.js +256 -129
  263. package/dist/drivers/gemini/index.js.map +1 -1
  264. package/dist/drivers/local/index.d.ts +2 -0
  265. package/dist/drivers/local/index.js +12 -0
  266. package/dist/drivers/local/index.js.map +1 -1
  267. package/dist/drivers/openai/index.js +30 -2
  268. package/dist/drivers/openai/index.js.map +1 -1
  269. package/dist/extensionClient.d.ts +37 -4
  270. package/dist/extensionClient.js +58 -16
  271. package/dist/extensionClient.js.map +1 -1
  272. package/dist/featureFlags.d.ts +91 -0
  273. package/dist/featureFlags.js +174 -3
  274. package/dist/featureFlags.js.map +1 -1
  275. package/dist/fileLock.js +21 -12
  276. package/dist/fileLock.js.map +1 -1
  277. package/dist/fileLockSync.d.ts +67 -0
  278. package/dist/fileLockSync.js +126 -0
  279. package/dist/fileLockSync.js.map +1 -0
  280. package/dist/fp/activityAnalytics.js +26 -12
  281. package/dist/fp/activityAnalytics.js.map +1 -1
  282. package/dist/fp/automationInterpreter.d.ts +15 -1
  283. package/dist/fp/automationInterpreter.js +267 -87
  284. package/dist/fp/automationInterpreter.js.map +1 -1
  285. package/dist/fp/automationProgram.d.ts +30 -0
  286. package/dist/fp/automationProgram.js.map +1 -1
  287. package/dist/fp/automationState.d.ts +24 -5
  288. package/dist/fp/automationState.js +56 -9
  289. package/dist/fp/automationState.js.map +1 -1
  290. package/dist/fp/automationUtils.js +1 -1
  291. package/dist/fp/automationUtils.js.map +1 -1
  292. package/dist/fp/commandDescription.js +7 -1
  293. package/dist/fp/commandDescription.js.map +1 -1
  294. package/dist/fp/extensionSnapshot.js +8 -2
  295. package/dist/fp/extensionSnapshot.js.map +1 -1
  296. package/dist/fp/interpreterContext.d.ts +66 -1
  297. package/dist/fp/interpreterContext.js +91 -1
  298. package/dist/fp/interpreterContext.js.map +1 -1
  299. package/dist/fp/policyParser.js +34 -2
  300. package/dist/fp/policyParser.js.map +1 -1
  301. package/dist/fsWatchWithFallback.d.ts +36 -0
  302. package/dist/fsWatchWithFallback.js +128 -0
  303. package/dist/fsWatchWithFallback.js.map +1 -0
  304. package/dist/haltPushDispatch.d.ts +33 -0
  305. package/dist/haltPushDispatch.js +103 -0
  306. package/dist/haltPushDispatch.js.map +1 -0
  307. package/dist/httpBodyValidation.d.ts +41 -0
  308. package/dist/httpBodyValidation.js +45 -0
  309. package/dist/httpBodyValidation.js.map +1 -0
  310. package/dist/inboxRoutes.d.ts +22 -0
  311. package/dist/inboxRoutes.js +61 -1
  312. package/dist/inboxRoutes.js.map +1 -1
  313. package/dist/index.js +1206 -158
  314. package/dist/index.js.map +1 -1
  315. package/dist/installGuard.js +6 -2
  316. package/dist/installGuard.js.map +1 -1
  317. package/dist/localEndpointGuard.d.ts +25 -0
  318. package/dist/localEndpointGuard.js +101 -0
  319. package/dist/localEndpointGuard.js.map +1 -0
  320. package/dist/lockfile.js +31 -4
  321. package/dist/lockfile.js.map +1 -1
  322. package/dist/oauth.d.ts +9 -0
  323. package/dist/oauth.js +33 -0
  324. package/dist/oauth.js.map +1 -1
  325. package/dist/oauthRoutes.d.ts +1 -1
  326. package/dist/oauthRoutes.js +5 -3
  327. package/dist/oauthRoutes.js.map +1 -1
  328. package/dist/orchestrator/childBridgeClient.d.ts +5 -0
  329. package/dist/orchestrator/childBridgeClient.js +28 -1
  330. package/dist/orchestrator/childBridgeClient.js.map +1 -1
  331. package/dist/orchestrator/childBridgeRegistry.js +29 -11
  332. package/dist/orchestrator/childBridgeRegistry.js.map +1 -1
  333. package/dist/orchestrator/orchestratorBridge.d.ts +33 -0
  334. package/dist/orchestrator/orchestratorBridge.js +70 -9
  335. package/dist/orchestrator/orchestratorBridge.js.map +1 -1
  336. package/dist/patchworkConfig.d.ts +44 -1
  337. package/dist/patchworkConfig.js +28 -3
  338. package/dist/patchworkConfig.js.map +1 -1
  339. package/dist/pluginLoader.js +10 -1
  340. package/dist/pluginLoader.js.map +1 -1
  341. package/dist/pluginWatcher.js +12 -14
  342. package/dist/pluginWatcher.js.map +1 -1
  343. package/dist/preToolUseHook.js +3 -2
  344. package/dist/preToolUseHook.js.map +1 -1
  345. package/dist/processTree.d.ts +34 -0
  346. package/dist/processTree.js +105 -0
  347. package/dist/processTree.js.map +1 -0
  348. package/dist/prompts.js +3 -3
  349. package/dist/prompts.js.map +1 -1
  350. package/dist/recipeOrchestration.d.ts +9 -0
  351. package/dist/recipeOrchestration.js +341 -24
  352. package/dist/recipeOrchestration.js.map +1 -1
  353. package/dist/recipeRoutes.d.ts +58 -2
  354. package/dist/recipeRoutes.js +744 -115
  355. package/dist/recipeRoutes.js.map +1 -1
  356. package/dist/recipes/RecipeOrchestrator.d.ts +2 -0
  357. package/dist/recipes/RecipeOrchestrator.js +6 -1
  358. package/dist/recipes/RecipeOrchestrator.js.map +1 -1
  359. package/dist/recipes/agentExecutor.d.ts +25 -5
  360. package/dist/recipes/agentExecutor.js.map +1 -1
  361. package/dist/recipes/chainedRunner.d.ts +2 -0
  362. package/dist/recipes/chainedRunner.js +156 -9
  363. package/dist/recipes/chainedRunner.js.map +1 -1
  364. package/dist/recipes/compiler.js +9 -5
  365. package/dist/recipes/compiler.js.map +1 -1
  366. package/dist/recipes/connectorPreflight.d.ts +66 -0
  367. package/dist/recipes/connectorPreflight.js +169 -0
  368. package/dist/recipes/connectorPreflight.js.map +1 -0
  369. package/dist/recipes/dependencyGraph.d.ts +9 -0
  370. package/dist/recipes/dependencyGraph.js +32 -7
  371. package/dist/recipes/dependencyGraph.js.map +1 -1
  372. package/dist/recipes/githubInstallSource.d.ts +135 -0
  373. package/dist/recipes/githubInstallSource.js +236 -0
  374. package/dist/recipes/githubInstallSource.js.map +1 -0
  375. package/dist/recipes/haltCategory.d.ts +119 -0
  376. package/dist/recipes/haltCategory.js +188 -0
  377. package/dist/recipes/haltCategory.js.map +1 -0
  378. package/dist/recipes/idempotencyKey.d.ts +154 -0
  379. package/dist/recipes/idempotencyKey.js +375 -0
  380. package/dist/recipes/idempotencyKey.js.map +1 -0
  381. package/dist/recipes/installer.js +58 -3
  382. package/dist/recipes/installer.js.map +1 -1
  383. package/dist/recipes/judgeSummary.d.ts +50 -0
  384. package/dist/recipes/judgeSummary.js +47 -0
  385. package/dist/recipes/judgeSummary.js.map +1 -0
  386. package/dist/recipes/judgeVerdict.d.ts +48 -0
  387. package/dist/recipes/judgeVerdict.js +174 -0
  388. package/dist/recipes/judgeVerdict.js.map +1 -0
  389. package/dist/recipes/migrations/index.d.ts +9 -0
  390. package/dist/recipes/migrations/index.js +133 -0
  391. package/dist/recipes/migrations/index.js.map +1 -1
  392. package/dist/recipes/names.d.ts +20 -0
  393. package/dist/recipes/names.js +25 -0
  394. package/dist/recipes/names.js.map +1 -1
  395. package/dist/recipes/parser.js +116 -6
  396. package/dist/recipes/parser.js.map +1 -1
  397. package/dist/recipes/replayRun.js +1 -1
  398. package/dist/recipes/replayRun.js.map +1 -1
  399. package/dist/recipes/runBudget.d.ts +70 -0
  400. package/dist/recipes/runBudget.js +109 -0
  401. package/dist/recipes/runBudget.js.map +1 -0
  402. package/dist/recipes/scheduler.d.ts +30 -0
  403. package/dist/recipes/scheduler.js +69 -19
  404. package/dist/recipes/scheduler.js.map +1 -1
  405. package/dist/recipes/schema.d.ts +65 -2
  406. package/dist/recipes/schemaGenerator.js +119 -7
  407. package/dist/recipes/schemaGenerator.js.map +1 -1
  408. package/dist/recipes/stepObservation.js +9 -0
  409. package/dist/recipes/stepObservation.js.map +1 -1
  410. package/dist/recipes/toolRegistry.js +33 -7
  411. package/dist/recipes/toolRegistry.js.map +1 -1
  412. package/dist/recipes/tools/airtable.d.ts +15 -0
  413. package/dist/recipes/tools/airtable.js +239 -0
  414. package/dist/recipes/tools/airtable.js.map +1 -0
  415. package/dist/recipes/tools/fanOut.d.ts +20 -0
  416. package/dist/recipes/tools/fanOut.js +199 -0
  417. package/dist/recipes/tools/fanOut.js.map +1 -0
  418. package/dist/recipes/tools/file.js +5 -2
  419. package/dist/recipes/tools/file.js.map +1 -1
  420. package/dist/recipes/tools/github.d.ts +1 -1
  421. package/dist/recipes/tools/github.js +75 -1
  422. package/dist/recipes/tools/github.js.map +1 -1
  423. package/dist/recipes/tools/gmail.js +27 -5
  424. package/dist/recipes/tools/gmail.js.map +1 -1
  425. package/dist/recipes/tools/googleDrive.js +64 -0
  426. package/dist/recipes/tools/googleDrive.js.map +1 -1
  427. package/dist/recipes/tools/http.d.ts +10 -0
  428. package/dist/recipes/tools/http.js +176 -0
  429. package/dist/recipes/tools/http.js.map +1 -0
  430. package/dist/recipes/tools/index.d.ts +7 -0
  431. package/dist/recipes/tools/index.js +7 -0
  432. package/dist/recipes/tools/index.js.map +1 -1
  433. package/dist/recipes/tools/resend.d.ts +8 -0
  434. package/dist/recipes/tools/resend.js +152 -0
  435. package/dist/recipes/tools/resend.js.map +1 -0
  436. package/dist/recipes/tools/sendgrid.d.ts +9 -0
  437. package/dist/recipes/tools/sendgrid.js +174 -0
  438. package/dist/recipes/tools/sendgrid.js.map +1 -0
  439. package/dist/recipes/tools/slack.js +1 -1
  440. package/dist/recipes/tools/twilio.d.ts +11 -0
  441. package/dist/recipes/tools/twilio.js +179 -0
  442. package/dist/recipes/tools/twilio.js.map +1 -0
  443. package/dist/recipes/tools/vercel.d.ts +9 -0
  444. package/dist/recipes/tools/vercel.js +145 -0
  445. package/dist/recipes/tools/vercel.js.map +1 -0
  446. package/dist/recipes/validation.d.ts +17 -0
  447. package/dist/recipes/validation.js +141 -11
  448. package/dist/recipes/validation.js.map +1 -1
  449. package/dist/recipes/workspaceRoot.d.ts +37 -0
  450. package/dist/recipes/workspaceRoot.js +73 -0
  451. package/dist/recipes/workspaceRoot.js.map +1 -0
  452. package/dist/recipes/yamlPositions.d.ts +56 -0
  453. package/dist/recipes/yamlPositions.js +183 -0
  454. package/dist/recipes/yamlPositions.js.map +1 -0
  455. package/dist/recipes/yamlRunner.d.ts +185 -8
  456. package/dist/recipes/yamlRunner.js +967 -224
  457. package/dist/recipes/yamlRunner.js.map +1 -1
  458. package/dist/recipesHttp.d.ts +19 -3
  459. package/dist/recipesHttp.js +67 -11
  460. package/dist/recipesHttp.js.map +1 -1
  461. package/dist/resources.js +21 -13
  462. package/dist/resources.js.map +1 -1
  463. package/dist/runLog.d.ts +58 -5
  464. package/dist/runLog.js +98 -21
  465. package/dist/runLog.js.map +1 -1
  466. package/dist/sanitizeParsedJson.d.ts +39 -0
  467. package/dist/sanitizeParsedJson.js +55 -0
  468. package/dist/sanitizeParsedJson.js.map +1 -0
  469. package/dist/schemas/recipe.v1.json +887 -198
  470. package/dist/server.d.ts +160 -3
  471. package/dist/server.js +1099 -122
  472. package/dist/server.js.map +1 -1
  473. package/dist/sessionCheckpoint.d.ts +8 -0
  474. package/dist/sessionCheckpoint.js +33 -3
  475. package/dist/sessionCheckpoint.js.map +1 -1
  476. package/dist/ssrfGuard.d.ts +16 -0
  477. package/dist/ssrfGuard.js +87 -4
  478. package/dist/ssrfGuard.js.map +1 -1
  479. package/dist/streamableHttp.d.ts +9 -4
  480. package/dist/streamableHttp.js +76 -20
  481. package/dist/streamableHttp.js.map +1 -1
  482. package/dist/telemetry.js +37 -19
  483. package/dist/telemetry.js.map +1 -1
  484. package/dist/testing/shadowRun.d.ts +52 -0
  485. package/dist/testing/shadowRun.js +71 -0
  486. package/dist/testing/shadowRun.js.map +1 -0
  487. package/dist/tools/batchLsp.d.ts +3 -0
  488. package/dist/tools/bridgeDoctor.d.ts +11 -0
  489. package/dist/tools/bridgeDoctor.js +48 -2
  490. package/dist/tools/bridgeDoctor.js.map +1 -1
  491. package/dist/tools/bridgeStatus.d.ts +0 -12
  492. package/dist/tools/bridgeStatus.js +2 -28
  493. package/dist/tools/bridgeStatus.js.map +1 -1
  494. package/dist/tools/cancelClaudeTask.d.ts +1 -0
  495. package/dist/tools/clipboard.d.ts +2 -0
  496. package/dist/tools/closeTabs.d.ts +1 -0
  497. package/dist/tools/codeLens.d.ts +1 -0
  498. package/dist/tools/createIssueFromAIComment.d.ts +1 -0
  499. package/dist/tools/ctxGetTaskContext.d.ts +9 -0
  500. package/dist/tools/ctxGetTaskContext.js +50 -2
  501. package/dist/tools/ctxGetTaskContext.js.map +1 -1
  502. package/dist/tools/ctxQueryTraces.js +32 -24
  503. package/dist/tools/ctxQueryTraces.js.map +1 -1
  504. package/dist/tools/ctxSaveTrace.d.ts +1 -0
  505. package/dist/tools/debug.d.ts +4 -0
  506. package/dist/tools/decorations.d.ts +2 -0
  507. package/dist/tools/detectUnusedCode.js +9 -7
  508. package/dist/tools/detectUnusedCode.js.map +1 -1
  509. package/dist/tools/documentLinks.d.ts +1 -0
  510. package/dist/tools/editText.d.ts +1 -0
  511. package/dist/tools/editText.js +2 -1
  512. package/dist/tools/editText.js.map +1 -1
  513. package/dist/tools/enrichCommit.d.ts +1 -0
  514. package/dist/tools/enrichStackTrace.js +11 -5
  515. package/dist/tools/enrichStackTrace.js.map +1 -1
  516. package/dist/tools/explainDiagnostic.d.ts +1 -0
  517. package/dist/tools/explainSymbol.d.ts +1 -0
  518. package/dist/tools/fileOperations.d.ts +3 -0
  519. package/dist/tools/fileOperations.js +2 -1
  520. package/dist/tools/fileOperations.js.map +1 -1
  521. package/dist/tools/fileWatcher.d.ts +2 -0
  522. package/dist/tools/fileWatcher.js +8 -2
  523. package/dist/tools/fileWatcher.js.map +1 -1
  524. package/dist/tools/findFiles.d.ts +1 -0
  525. package/dist/tools/fixAllLintErrors.d.ts +1 -0
  526. package/dist/tools/fixAllLintErrors.js +10 -5
  527. package/dist/tools/fixAllLintErrors.js.map +1 -1
  528. package/dist/tools/foldingRanges.d.ts +1 -0
  529. package/dist/tools/formatDocument.d.ts +1 -0
  530. package/dist/tools/formatDocument.js +10 -5
  531. package/dist/tools/formatDocument.js.map +1 -1
  532. package/dist/tools/generateTests.d.ts +1 -0
  533. package/dist/tools/getAIComments.d.ts +1 -0
  534. package/dist/tools/getAnalyticsReport.js +5 -1
  535. package/dist/tools/getAnalyticsReport.js.map +1 -1
  536. package/dist/tools/getBufferContent.d.ts +1 -0
  537. package/dist/tools/getChangeImpact.d.ts +1 -0
  538. package/dist/tools/getChangeImpact.js +23 -14
  539. package/dist/tools/getChangeImpact.js.map +1 -1
  540. package/dist/tools/getClaudeTaskStatus.d.ts +1 -0
  541. package/dist/tools/getCodeCoverage.d.ts +1 -0
  542. package/dist/tools/getCodeCoverage.js +32 -14
  543. package/dist/tools/getCodeCoverage.js.map +1 -1
  544. package/dist/tools/getCommitsForIssue.d.ts +1 -0
  545. package/dist/tools/getDebugState.d.ts +1 -0
  546. package/dist/tools/getDiagnostics.js +32 -29
  547. package/dist/tools/getDiagnostics.js.map +1 -1
  548. package/dist/tools/getDiffFromHandoff.js +8 -2
  549. package/dist/tools/getDiffFromHandoff.js.map +1 -1
  550. package/dist/tools/getDocumentSymbols.d.ts +1 -0
  551. package/dist/tools/getGitHotspots.d.ts +1 -0
  552. package/dist/tools/getGitStatus.js +6 -1
  553. package/dist/tools/getGitStatus.js.map +1 -1
  554. package/dist/tools/getImportedSignatures.d.ts +1 -0
  555. package/dist/tools/getImportedSignatures.js +18 -14
  556. package/dist/tools/getImportedSignatures.js.map +1 -1
  557. package/dist/tools/getPRTemplate.d.ts +1 -0
  558. package/dist/tools/getProjectContext.js +23 -10
  559. package/dist/tools/getProjectContext.js.map +1 -1
  560. package/dist/tools/getSymbolHistory.d.ts +1 -0
  561. package/dist/tools/getToolCapabilities.d.ts +10 -5
  562. package/dist/tools/getToolCapabilities.js +116 -204
  563. package/dist/tools/getToolCapabilities.js.map +1 -1
  564. package/dist/tools/getTypeSignature.d.ts +1 -0
  565. package/dist/tools/getWorkspaceSettings.d.ts +1 -0
  566. package/dist/tools/gitWrite.d.ts +11 -0
  567. package/dist/tools/github/actions.d.ts +2 -0
  568. package/dist/tools/github/composite.d.ts +4 -1
  569. package/dist/tools/github/composite.js +7 -7
  570. package/dist/tools/github/composite.js.map +1 -1
  571. package/dist/tools/github/issues.d.ts +4 -0
  572. package/dist/tools/github/pr.d.ts +13 -6
  573. package/dist/tools/github/pr.js +18 -11
  574. package/dist/tools/github/pr.js.map +1 -1
  575. package/dist/tools/handoffNote.d.ts +1 -0
  576. package/dist/tools/handoffNote.js +2 -1
  577. package/dist/tools/handoffNote.js.map +1 -1
  578. package/dist/tools/headless/lspClient.js +3 -0
  579. package/dist/tools/headless/lspClient.js.map +1 -1
  580. package/dist/tools/hoverAtCursor.d.ts +1 -0
  581. package/dist/tools/httpClient.d.ts +2 -0
  582. package/dist/tools/httpClient.js +38 -71
  583. package/dist/tools/httpClient.js.map +1 -1
  584. package/dist/tools/index.js +14 -10
  585. package/dist/tools/index.js.map +1 -1
  586. package/dist/tools/inlayHints.d.ts +1 -0
  587. package/dist/tools/launchQuickTask.d.ts +1 -0
  588. package/dist/tools/listClaudeTasks.d.ts +1 -0
  589. package/dist/tools/listClaudeTasks.js +10 -8
  590. package/dist/tools/listClaudeTasks.js.map +1 -1
  591. package/dist/tools/listTerminals.d.ts +1 -0
  592. package/dist/tools/lsp.d.ts +15 -0
  593. package/dist/tools/lsp.js +17 -0
  594. package/dist/tools/lsp.js.map +1 -1
  595. package/dist/tools/navigateToSymbolByName.d.ts +1 -0
  596. package/dist/tools/openDiff.d.ts +1 -0
  597. package/dist/tools/openDiff.js +4 -1
  598. package/dist/tools/openDiff.js.map +1 -1
  599. package/dist/tools/openFile.d.ts +1 -0
  600. package/dist/tools/openFile.js +4 -1
  601. package/dist/tools/openFile.js.map +1 -1
  602. package/dist/tools/openInBrowser.js +6 -1
  603. package/dist/tools/openInBrowser.js.map +1 -1
  604. package/dist/tools/organizeImports.d.ts +1 -0
  605. package/dist/tools/organizeImports.js +5 -3
  606. package/dist/tools/organizeImports.js.map +1 -1
  607. package/dist/tools/performanceReport.js +5 -3
  608. package/dist/tools/performanceReport.js.map +1 -1
  609. package/dist/tools/planPersistence.d.ts +3 -0
  610. package/dist/tools/planPersistence.js +4 -1
  611. package/dist/tools/planPersistence.js.map +1 -1
  612. package/dist/tools/previewEdit.d.ts +1 -0
  613. package/dist/tools/previewEdit.js +31 -5
  614. package/dist/tools/previewEdit.js.map +1 -1
  615. package/dist/tools/recentTracesDigest.js +54 -11
  616. package/dist/tools/recentTracesDigest.js.map +1 -1
  617. package/dist/tools/refactorAnalyze.d.ts +1 -0
  618. package/dist/tools/refactorExtractFunction.js +4 -1
  619. package/dist/tools/refactorExtractFunction.js.map +1 -1
  620. package/dist/tools/refactorPreview.d.ts +1 -0
  621. package/dist/tools/refactorPreview.js +10 -2
  622. package/dist/tools/refactorPreview.js.map +1 -1
  623. package/dist/tools/replaceBlock.d.ts +1 -0
  624. package/dist/tools/replaceBlock.js +2 -1
  625. package/dist/tools/replaceBlock.js.map +1 -1
  626. package/dist/tools/resumeClaudeTask.d.ts +1 -0
  627. package/dist/tools/runClaudeTask.d.ts +1 -0
  628. package/dist/tools/runTests.js +15 -4
  629. package/dist/tools/runTests.js.map +1 -1
  630. package/dist/tools/screenshot.d.ts +1 -0
  631. package/dist/tools/screenshotAndAnnotate.js +6 -2
  632. package/dist/tools/screenshotAndAnnotate.js.map +1 -1
  633. package/dist/tools/searchAndReplace.d.ts +1 -0
  634. package/dist/tools/searchAndReplace.js +21 -12
  635. package/dist/tools/searchAndReplace.js.map +1 -1
  636. package/dist/tools/searchTools.d.ts +26 -0
  637. package/dist/tools/searchTools.js +62 -21
  638. package/dist/tools/searchTools.js.map +1 -1
  639. package/dist/tools/searchWorkspace.d.ts +1 -0
  640. package/dist/tools/selectionRanges.d.ts +1 -0
  641. package/dist/tools/semanticTokens.d.ts +1 -0
  642. package/dist/tools/signatureHelp.d.ts +1 -0
  643. package/dist/tools/spawnWorkspace.js +15 -7
  644. package/dist/tools/spawnWorkspace.js.map +1 -1
  645. package/dist/tools/terminal.d.ts +6 -0
  646. package/dist/tools/testRunners/pytest.js +6 -2
  647. package/dist/tools/testRunners/pytest.js.map +1 -1
  648. package/dist/tools/testRunners/vitestJest.js +3 -1
  649. package/dist/tools/testRunners/vitestJest.js.map +1 -1
  650. package/dist/tools/testTraceToSource.d.ts +1 -0
  651. package/dist/tools/transaction.d.ts +13 -1
  652. package/dist/tools/transaction.js +63 -5
  653. package/dist/tools/transaction.js.map +1 -1
  654. package/dist/tools/typeHierarchy.d.ts +1 -0
  655. package/dist/tools/utils.d.ts +33 -0
  656. package/dist/tools/utils.js +122 -18
  657. package/dist/tools/utils.js.map +1 -1
  658. package/dist/tools/vscodeCommands.d.ts +2 -0
  659. package/dist/tools/vscodeTasks.d.ts +2 -0
  660. package/dist/tools/workspaceSettings.d.ts +1 -0
  661. package/dist/transport.d.ts +3 -1
  662. package/dist/transport.js +126 -52
  663. package/dist/transport.js.map +1 -1
  664. package/dist/winShim.d.ts +34 -0
  665. package/dist/winShim.js +94 -0
  666. package/dist/winShim.js.map +1 -0
  667. package/dist/wireHaltPushDispatch.d.ts +38 -0
  668. package/dist/wireHaltPushDispatch.js +71 -0
  669. package/dist/wireHaltPushDispatch.js.map +1 -0
  670. package/dist/writeFileAtomic.d.ts +23 -0
  671. package/dist/writeFileAtomic.js +121 -0
  672. package/dist/writeFileAtomic.js.map +1 -0
  673. package/package.json +23 -7
  674. package/scripts/postinstall.mjs +42 -2
  675. package/scripts/smoke/run-all.mjs +213 -0
  676. package/scripts/start-all.mjs +572 -0
  677. package/scripts/start-all.ps1 +209 -0
  678. package/scripts/start-all.sh +73 -17
  679. package/scripts/start-orchestrator.ps1 +158 -0
  680. package/scripts/start-remote.mjs +122 -0
  681. package/templates/automation-policies/recipe-authoring.json +1 -1
  682. package/templates/automation-policies/security-first.json +1 -1
  683. package/templates/automation-policies/strict-lint.json +1 -1
  684. package/templates/automation-policies/test-driven.json +1 -1
  685. package/templates/automation-policy.example.json +1 -1
  686. package/templates/co.patchwork-os.bridge.plist +2 -2
  687. package/templates/recipes/approval-queue-ui-test.yaml +1 -1
  688. package/templates/recipes/ctx-loop-test.yaml +1 -1
  689. package/templates/recipes/fix-errors-on-save.yaml +71 -0
  690. package/templates/recipes/morning-brief.yaml +5 -2
  691. package/templates/recipes/project-health-check.yaml +4 -1
  692. package/templates/recipes/sentry-to-linear.yaml +72 -38
  693. package/templates/recipes/webhook/apple-watch-health-log.yaml +145 -0
  694. package/templates/recipes/webhook/customer-escalation.yaml +8 -9
  695. package/templates/recipes/webhook/meeting-prep.yaml +11 -5
  696. package/dist/commands/marketplace.d.ts +0 -16
  697. package/dist/commands/marketplace.js +0 -32
  698. package/dist/commands/marketplace.js.map +0 -1
  699. package/dist/recipes/legacyRecipeCompat.d.ts +0 -10
  700. package/dist/recipes/legacyRecipeCompat.js +0 -131
  701. package/dist/recipes/legacyRecipeCompat.js.map +0 -1
package/dist/index.js CHANGED
@@ -38,10 +38,14 @@ import readline from "node:readline";
38
38
  import { fileURLToPath } from "node:url";
39
39
  import { getAnalyticsPref, setAnalyticsPref } from "./analyticsPrefs.js";
40
40
  import { Bridge } from "./bridge.js";
41
+ import { findAllLiveBridges, findBridgeLock, } from "./bridgeLockDiscovery.js";
41
42
  import { isBridgeToolsFileValid, repairBridgeToolsRulesIfStale, } from "./bridgeToolsRules.js";
42
43
  import { findEditor, parseConfig } from "./config.js";
43
44
  import { detectWorkspaceSymlinkInstall, PATCHWORK_PACKAGE_NAME, SYMLINK_INSTALL_FIX, } from "./installGuard.js";
45
+ import { treeKill } from "./processTree.js";
44
46
  import { PACKAGE_VERSION, semverGt } from "./version.js";
47
+ import { ensureCmdShim } from "./winShim.js";
48
+ import { writeFileAtomicSync } from "./writeFileAtomic.js";
45
49
  const __dirnameTop = path.dirname(fileURLToPath(import.meta.url));
46
50
  // Warn when a symlinked global install is detected (`npm install -g .`).
47
51
  // launchctl / sandbox environments can fail through that link with EPERM.
@@ -119,15 +123,26 @@ const KNOWN_SUBCOMMANDS = [
119
123
  "gen-plugin-stub",
120
124
  "notify",
121
125
  "install",
122
- "marketplace",
123
126
  "status",
124
127
  "shim",
128
+ "quick-task",
129
+ "start-task",
130
+ "continue-handoff",
131
+ "token-efficiency",
125
132
  "recipe",
133
+ "connect",
126
134
  "traces",
127
135
  "suggest",
128
136
  "dashboard",
129
137
  "launchd",
130
138
  "start",
139
+ "kill-switch",
140
+ "panic",
141
+ "halts",
142
+ "judgments",
143
+ "analytics",
144
+ "doctor",
145
+ "shadow-scan",
131
146
  ];
132
147
  const __invokedSubcommand = (() => {
133
148
  const sub = process.argv[2];
@@ -139,10 +154,20 @@ const __invokedSubcommand = (() => {
139
154
  ? sub
140
155
  : null;
141
156
  })();
157
+ // bash/zsh set process.env._ to the actual invoked binary path (e.g. /usr/local/bin/patchwork-os).
158
+ // More reliable than argv[1] which resolves to the .js entrypoint via npm global shim.
159
+ function invokedBinaryName() {
160
+ const fromEnv = process.env._
161
+ ? path.basename(process.env._).replace(/\.(cmd|js)$/i, "")
162
+ : "";
163
+ if (fromEnv && fromEnv !== "node" && fromEnv !== "npm")
164
+ return fromEnv;
165
+ return path.basename(process.argv[1] ?? "").replace(/\.js$/, "");
166
+ }
142
167
  const __invokedBareBinaryDashboard = (() => {
143
168
  if (process.argv[2] && process.argv[2] !== "dashboard")
144
169
  return false;
145
- const binName = path.basename(process.argv[1] ?? "");
170
+ const binName = invokedBinaryName();
146
171
  return (binName === "patchwork-os" ||
147
172
  binName === "patchwork" ||
148
173
  binName === "patchwork.js");
@@ -153,6 +178,61 @@ if (process.argv[2] === "--version" || process.argv[2] === "-v") {
153
178
  console.log(`claude-ide-bridge ${PACKAGE_VERSION}`);
154
179
  process.exit(0);
155
180
  }
181
+ // Handle top-level --help / -h / help — print a grouped command index so a
182
+ // first-time user has a discoverable entry point. Without this, bare
183
+ // `patchwork --help` falls through to bridge-daemon arg parsing and errors.
184
+ if (process.argv[2] === "--help" ||
185
+ process.argv[2] === "-h" ||
186
+ process.argv[2] === "help") {
187
+ const binName = path.basename(process.argv[1] ?? "patchwork");
188
+ process.stdout.write(`${binName} ${PACKAGE_VERSION}\n\n` +
189
+ `First time? Run:\n` +
190
+ ` ${binName} init # set up ~/.patchwork + Claude Code hooks\n` +
191
+ ` ${binName} start-all # bridge + Claude + dashboard\n\n` +
192
+ `Get started\n` +
193
+ ` init [--workspace <dir>] Scaffold ~/.patchwork; register CC hooks\n` +
194
+ ` install-extension Install the VS Code / Cursor / Windsurf extension\n` +
195
+ ` start-all [--no-dashboard] Launch bridge + Claude --ide + dashboard\n` +
196
+ ` start-orchestrator Multi-IDE-window meta-bridge\n\n` +
197
+ `Recipes\n` +
198
+ ` recipe new <name> [-i] Scaffold a recipe\n` +
199
+ ` recipe list List installed recipes\n` +
200
+ ` recipe run <name> [--vars k=v] Run a recipe by name\n` +
201
+ ` recipe install <source> Install from a path or GitHub source\n` +
202
+ ` recipe --help Full recipe subcommand index\n\n` +
203
+ `Connectors\n` +
204
+ ` connect [list] List connectors + connection status\n` +
205
+ ` connect <vendor> OAuth: print authorize URL / PAT: connect\n` +
206
+ ` connect test <vendor> Health-probe a connector\n` +
207
+ ` connect disconnect <vendor> Revoke a connector\n\n` +
208
+ `Operate\n` +
209
+ ` start [--port N] [--workspace <dir>] Start a single bridge (no tmux)\n` +
210
+ ` status One-line bridge status (port, uptime)\n` +
211
+ ` tools [list|search <q>] [--slim] [--json] List tools the bridge would register\n` +
212
+ ` analytics <show|configure|clear|test> Manage opt-in telemetry config\n` +
213
+ ` launchd <install|uninstall|status> Manage the macOS auto-start LaunchAgent\n\n` +
214
+ `Diagnose\n` +
215
+ ` halts [--window 1h|24h|overnight|7d] Morning summary of recent recipe halts\n` +
216
+ ` judgments [--window ...] [--recipe N] Recent judge-step verdicts across runs\n` +
217
+ ` suggest [--since-days N] Recipe + unused-tool suggestions\n` +
218
+ ` token-efficiency benchmark Measure token cost across tool sets\n` +
219
+ ` traces export Bundle approval / recipe / decision traces\n` +
220
+ ` print-token [--port N] Print the active bridge auth token\n\n` +
221
+ `Safety\n` +
222
+ ` kill-switch <engage|release|status> Block / resume write-tier tools across bridges\n` +
223
+ ` panic [--reason "..."] Shorthand for kill-switch engage\n\n` +
224
+ `Daemon (no subcommand)\n` +
225
+ ` --workspace <dir> Start the bridge in foreground\n` +
226
+ ` --watch Auto-restart supervisor\n` +
227
+ ` --slim 27 IDE-only tools (default: full)\n\n` +
228
+ `Other\n` +
229
+ ` --version, -v Print package version\n` +
230
+ ` shim stdio↔WebSocket shim (used by MCP clients)\n` +
231
+ ` notify <event> Notify a running bridge of a CC hook event\n\n` +
232
+ `Bridge-daemon flags: run \`${binName} --workspace . --help-flags\` for the full list,\n` +
233
+ `or see https://github.com/Oolab-labs/patchwork-os#readme.\n`);
234
+ process.exit(0);
235
+ }
156
236
  // Handle patchwork-init subcommand — T2 from docs/install-ux-plan.md.
157
237
  // Separate from the bridge-only `init` to preserve back-compat. See ADR-0008.
158
238
  if (process.argv[2] === "patchwork-init") {
@@ -160,6 +240,13 @@ if (process.argv[2] === "patchwork-init") {
160
240
  await runPatchworkInit(process.argv.slice(3));
161
241
  process.exit(0);
162
242
  }
243
+ // `patchwork-os init` → dashboard setup, not IDE bridge installer.
244
+ // patchwork init / claude-ide-bridge init still go to the bridge path below.
245
+ if (process.argv[2] === "init" && invokedBinaryName() === "patchwork-os") {
246
+ const { runPatchworkInit } = await import("./commands/patchworkInit.js");
247
+ await runPatchworkInit(process.argv.slice(3));
248
+ process.exit(0);
249
+ }
163
250
  // Handle start-all subcommand — launches the full 3-pane tmux orchestrator.
164
251
  // Also triggered when invoked as `claude-ide-bridge-start` directly.
165
252
  const isStartAll = process.argv[2] === "start-all" ||
@@ -168,8 +255,11 @@ if (isStartAll) {
168
255
  const startAllArgs = process.argv[2] === "start-all"
169
256
  ? process.argv.slice(3)
170
257
  : process.argv.slice(2);
171
- const scriptPath = path.resolve(__dirnameTop, "..", "scripts", "start-all.sh");
172
- const result = spawnSync("bash", [scriptPath, ...startAllArgs], {
258
+ // Dispatch the cross-platform Node orchestrator (start-all.mjs). The
259
+ // bash entry-point is kept as a developer shortcut but Windows has no
260
+ // `bash` on PATH by default, and the .mjs is functionally equivalent.
261
+ const scriptPath = path.resolve(__dirnameTop, "..", "scripts", "start-all.mjs");
262
+ const result = spawnSync(process.execPath, [scriptPath, ...startAllArgs], {
173
263
  stdio: "inherit",
174
264
  });
175
265
  process.exit(result.status ?? 1);
@@ -177,14 +267,16 @@ if (isStartAll) {
177
267
  // `patchwork start` — opinionated front door over start-all.
178
268
  // Defaults to full mode (all tools registered) and the web dashboard, so the
179
269
  // doc-promised "patchwork start → everything works" path actually works.
180
- // Pass-through args still go to start-all.sh; --help short-circuits.
270
+ // Pass-through args still go to start-all.mjs; --help short-circuits.
181
271
  if (process.argv[2] === "start") {
182
272
  const passthrough = process.argv.slice(3);
183
273
  if (passthrough.includes("--help") || passthrough.includes("-h")) {
184
274
  process.stdout.write(`patchwork start — Launch the full Patchwork stack
185
275
 
186
- Starts bridge + Claude Code + dashboard in a tmux session.
276
+ Starts bridge + Claude + dashboard via the cross-platform Node orchestrator.
187
277
  Defaults to full mode so all bridge tools are registered.
278
+ On macOS/Linux: uses tmux when available, falls back to background mode.
279
+ On Windows: runs natively via the Node orchestrator (no WSL required).
188
280
 
189
281
  Usage: patchwork start [options]
190
282
 
@@ -206,13 +298,20 @@ This is a thin wrapper over \`start-all\`. For advanced flags see:
206
298
  const args = [...passthrough];
207
299
  const slimIdx = args.indexOf("--slim");
208
300
  if (slimIdx >= 0) {
209
- args.splice(slimIdx, 1); // strip start-all.sh has no --slim flag, slim is its default
301
+ args.splice(slimIdx, 1); // slim is the .mjs default; strip so --full isn't re-added below
210
302
  }
211
303
  else if (!args.includes("--full")) {
212
304
  args.push("--full");
213
305
  }
214
- const scriptPath = path.resolve(__dirnameTop, "..", "scripts", "start-all.sh");
215
- const result = spawnSync("bash", [scriptPath, ...args], {
306
+ // On non-Windows: auto-detect tmux; fall back to --no-tmux background mode if absent.
307
+ if (process.platform !== "win32" && !args.includes("--no-tmux")) {
308
+ const tmuxCheck = spawnSync("which", ["tmux"], { stdio: "ignore" });
309
+ if (tmuxCheck.status !== 0)
310
+ args.push("--no-tmux");
311
+ }
312
+ // Dispatch to the cross-platform Node orchestrator (see above).
313
+ const scriptPath = path.resolve(__dirnameTop, "..", "scripts", "start-all.mjs");
314
+ const result = spawnSync(process.execPath, [scriptPath, ...args], {
216
315
  stdio: "inherit",
217
316
  });
218
317
  process.exit(result.status ?? 1);
@@ -368,13 +467,6 @@ if (process.argv[2] === "install") {
368
467
  await runInstall(process.argv.slice(3));
369
468
  process.exit(0);
370
469
  }
371
- // Handle marketplace subcommand — DEPRECATED, prints migration message.
372
- // See issue #279 and src/commands/marketplace.ts for the rationale.
373
- if (process.argv[2] === "marketplace") {
374
- const { runMarketplace } = await import("./commands/marketplace.js");
375
- await runMarketplace(process.argv.slice(3));
376
- process.exit(0);
377
- }
378
470
  // Handle tools subcommand — search/list tools without a bridge connection
379
471
  if (process.argv[2] === "tools") {
380
472
  const { runToolsCommand } = await import("./commands/tools.js");
@@ -412,50 +504,31 @@ Options:
412
504
  const portIdx = argv.indexOf("--port");
413
505
  const portArg = portIdx !== -1 ? argv[portIdx + 1] : undefined;
414
506
  const lockDir = path.join(process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude"), "ide");
415
- let lockFile;
507
+ // Select a *running bridge* lock (isBridge:true + live PID) rather than the
508
+ // most-recently-touched lock in ~/.claude/ide, which can be an IDE-owned
509
+ // lock or a dead bridge.
510
+ let bridgeLock;
416
511
  if (portArg) {
417
- lockFile = path.join(lockDir, `${portArg}.lock`);
418
- if (!existsSync(lockFile)) {
419
- process.stderr.write(`Error: No lock file found for port ${portArg} at ${lockFile}\n`);
512
+ const port = Number(portArg);
513
+ bridgeLock = findAllLiveBridges().find((b) => b.port === port) ?? null;
514
+ if (!bridgeLock) {
515
+ process.stderr.write(`Error: No running bridge found for port ${portArg} (no lock file for that port, the lock is IDE-owned, or its process is not alive).\n`);
420
516
  process.exit(1);
421
517
  }
422
518
  }
423
519
  else {
424
- // Find the most recently modified lock file
425
- let bestMtime = 0;
426
- try {
427
- for (const f of readdirSync(lockDir)) {
428
- if (!f.endsWith(".lock"))
429
- continue;
430
- const full = path.join(lockDir, f);
431
- const mtime = statSync(full).mtimeMs;
432
- if (mtime > bestMtime) {
433
- bestMtime = mtime;
434
- lockFile = full;
435
- }
436
- }
437
- }
438
- catch {
439
- // lock dir doesn't exist — handled below
440
- }
520
+ bridgeLock = findBridgeLock();
441
521
  }
442
- if (!lockFile) {
443
- process.stderr.write(`Error: No bridge lock file found in ${lockDir}\n`);
522
+ if (!bridgeLock) {
523
+ process.stderr.write(`Error: No running bridge lock file found in ${lockDir}\n`);
444
524
  process.stderr.write("Make sure the bridge is running first, or pass --port <port>.\n");
445
525
  process.exit(1);
446
526
  }
447
- try {
448
- const data = JSON.parse(readFileSync(lockFile, "utf-8"));
449
- if (!data.authToken) {
450
- process.stderr.write(`Error: Lock file ${lockFile} has no authToken field\n`);
451
- process.exit(1);
452
- }
453
- process.stdout.write(`${data.authToken}\n`);
454
- }
455
- catch {
456
- process.stderr.write(`Error: Could not read lock file ${lockFile}\n`);
527
+ if (!bridgeLock.authToken) {
528
+ process.stderr.write(`Error: Bridge lock for port ${bridgeLock.port} has no authToken field\n`);
457
529
  process.exit(1);
458
530
  }
531
+ process.stdout.write(`${bridgeLock.authToken}\n`);
459
532
  process.exit(0);
460
533
  }
461
534
  // Handle notify subcommand — called from CC hooks to fire bridge automation
@@ -487,53 +560,32 @@ if (process.argv[2] === "notify") {
487
560
  }
488
561
  }
489
562
  const notifyLockDir = path.join(process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude"), "ide");
490
- let notifyLockFile;
563
+ // Select a *running bridge* lock (isBridge:true + live PID) rather than the
564
+ // most-recently-touched lock, which can be an IDE-owned or dead-bridge lock.
491
565
  let notifyPort;
566
+ let notifyToken;
492
567
  if (namedArgs.port) {
493
- notifyPort = Number(namedArgs.port);
494
- notifyLockFile = path.join(notifyLockDir, `${notifyPort}.lock`);
495
- if (!existsSync(notifyLockFile)) {
496
- process.stderr.write(`Error: No lock file found for port ${notifyPort}\n`);
568
+ const port = Number(namedArgs.port);
569
+ const lock = findAllLiveBridges().find((b) => b.port === port);
570
+ if (!lock) {
571
+ process.stderr.write(`Error: No running bridge found for port ${namedArgs.port} (no lock for that port, the lock is IDE-owned, or its process is not alive).\n`);
497
572
  process.exit(1);
498
573
  }
574
+ notifyPort = lock.port;
575
+ notifyToken = lock.authToken;
499
576
  }
500
577
  else {
501
- let bestMtime = 0;
502
- try {
503
- for (const f of readdirSync(notifyLockDir)) {
504
- if (!f.endsWith(".lock"))
505
- continue;
506
- const full = path.join(notifyLockDir, f);
507
- const mtime = statSync(full).mtimeMs;
508
- if (mtime > bestMtime) {
509
- bestMtime = mtime;
510
- notifyLockFile = full;
511
- notifyPort = Number(path.basename(f, ".lock"));
512
- }
513
- }
514
- }
515
- catch {
516
- // lock dir doesn't exist
578
+ const lock = findBridgeLock();
579
+ if (lock) {
580
+ notifyPort = lock.port;
581
+ notifyToken = lock.authToken;
517
582
  }
518
583
  }
519
- if (!notifyLockFile || !notifyPort) {
520
- process.stderr.write(`Error: No bridge lock file found in ${notifyLockDir}\n`);
584
+ if (!notifyPort || !notifyToken) {
585
+ process.stderr.write(`Error: No running bridge lock file found in ${notifyLockDir}\n`);
521
586
  process.stderr.write("Make sure the bridge is running first (claude-ide-bridge --watch ...).\n");
522
587
  process.exit(1);
523
588
  }
524
- let notifyToken;
525
- try {
526
- const data = JSON.parse(readFileSync(notifyLockFile, "utf-8"));
527
- if (!data.authToken) {
528
- process.stderr.write(`Error: Lock file has no authToken field\n`);
529
- process.exit(1);
530
- }
531
- notifyToken = data.authToken;
532
- }
533
- catch {
534
- process.stderr.write(`Error: Could not read lock file ${notifyLockFile}\n`);
535
- process.exit(1);
536
- }
537
589
  try {
538
590
  const resp = await fetch(`http://127.0.0.1:${notifyPort}/notify`, {
539
591
  method: "POST",
@@ -885,6 +937,33 @@ Edit, save, hot-reload — Claude's next turn sees the new tool. See [documents/
885
937
  }
886
938
  process.exit(0);
887
939
  }
940
+ // Patchwork: `patchwork recipe` (no subcommand) / `recipe --help` — print
941
+ // the subcommand index. Without this branch, `patchwork recipe` falls through
942
+ // to the bridge daemon, leaving subcommands completely undiscoverable from
943
+ // the CLI (the only way to find them today is to read CLAUDE.md or source).
944
+ if (process.argv[2] === "recipe" &&
945
+ (process.argv[3] === undefined ||
946
+ process.argv[3] === "--help" ||
947
+ process.argv[3] === "-h" ||
948
+ process.argv[3] === "help")) {
949
+ process.stdout.write(`Usage: patchwork recipe <subcommand> [args...]\n\n` +
950
+ `Subcommands:\n` +
951
+ ` new <name> Scaffold a recipe (interactive with -i)\n` +
952
+ ` list List installed recipes (workspace + user)\n` +
953
+ ` run <name> Run a recipe by name\n` +
954
+ ` install <src> Install a recipe from a path or GitHub source\n` +
955
+ ` uninstall <name> Remove an installed recipe\n` +
956
+ ` enable <name> Re-enable a disabled recipe\n` +
957
+ ` disable <name> Pause a recipe (scheduled triggers stop firing)\n` +
958
+ ` preflight <file> Static-validate a recipe YAML before running\n` +
959
+ ` doctor <name|file> Diagnose a recipe: lint + policy + recent halts\n` +
960
+ ` lint <file> Run all lint checks on a recipe YAML\n` +
961
+ ` fmt <file> Format a recipe YAML in place\n` +
962
+ ` schema Print the recipe JSON Schema\n` +
963
+ ` audit-env <recipe> Check {{env.FOO}} vars are present in environment\n\n` +
964
+ `Run \`patchwork recipe <subcommand> --help\` for subcommand-specific options.\n`);
965
+ process.exit(0);
966
+ }
888
967
  // Patchwork: `patchwork recipe list` — enumerate installed recipes.
889
968
  if (process.argv[2] === "recipe" && process.argv[3] === "list") {
890
969
  (async () => {
@@ -960,11 +1039,13 @@ if (process.argv[2] === "recipe" && process.argv[3] === "uninstall") {
960
1039
  // a running bridge's /recipes/run endpoint if one is available.
961
1040
  if (process.argv[2] === "recipe" && process.argv[3] === "run") {
962
1041
  const args = process.argv.slice(4);
963
- const usage = "Usage: patchwork recipe run <name-or-file> [--local] [--dry-run] [--step <id>] [--var KEY=VALUE]\n";
1042
+ const usage = "Usage: patchwork recipe run <name-or-file> [--local] [--dry-run] [--step <id>] [--var KEY=VALUE] [--attempt <id>] [--ledger-dir <path>]\n";
964
1043
  let localFlag = false;
965
1044
  let dryRun = false;
966
1045
  let recipeRef;
967
1046
  let step;
1047
+ let attemptId;
1048
+ let ledgerDir;
968
1049
  const vars = {};
969
1050
  for (let i = 0; i < args.length; i++) {
970
1051
  const arg = args[i];
@@ -991,6 +1072,29 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
991
1072
  step = value;
992
1073
  continue;
993
1074
  }
1075
+ if (currentArg === "--attempt" || currentArg.startsWith("--attempt=")) {
1076
+ const value = currentArg === "--attempt"
1077
+ ? args[++i]
1078
+ : currentArg.slice("--attempt=".length);
1079
+ if (!value) {
1080
+ process.stderr.write(`Error: --attempt requires a value\n${usage}`);
1081
+ process.exit(1);
1082
+ }
1083
+ attemptId = value;
1084
+ continue;
1085
+ }
1086
+ if (currentArg === "--ledger-dir" ||
1087
+ currentArg.startsWith("--ledger-dir=")) {
1088
+ const value = currentArg === "--ledger-dir"
1089
+ ? args[++i]
1090
+ : currentArg.slice("--ledger-dir=".length);
1091
+ if (!value) {
1092
+ process.stderr.write(`Error: --ledger-dir requires a value\n${usage}`);
1093
+ process.exit(1);
1094
+ }
1095
+ ledgerDir = value;
1096
+ continue;
1097
+ }
994
1098
  if (currentArg === "--var" || currentArg.startsWith("--var=")) {
995
1099
  const assignment = currentArg === "--var" ? args[++i] : currentArg.slice("--var=".length);
996
1100
  if (!assignment) {
@@ -1037,19 +1141,42 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
1037
1141
  })();
1038
1142
  const { findBridgeLock } = await import("./bridgeLockDiscovery.js");
1039
1143
  const lock = localFlag ? null : findBridgeLock();
1040
- if (lock && !dryRun && !step && !explicitFile) {
1041
- const res = await fetch(`http://127.0.0.1:${lock.port}/recipes/run`, {
1042
- method: "POST",
1043
- headers: {
1044
- Authorization: `Bearer ${lock.authToken}`,
1045
- "Content-Type": "application/json",
1046
- },
1047
- body: JSON.stringify({
1048
- name: recipeArg,
1049
- ...(seedVars ? { vars: seedVars } : {}),
1050
- }),
1051
- });
1052
- const body = (await res.json());
1144
+ if (lock && !dryRun && !step && !explicitFile && !attemptId) {
1145
+ // 30s per-request deadline the bridge can pass the findBridgeLock
1146
+ // PID check yet be wedged on HTTP. Without this abort the fetch
1147
+ // blocks forever. Mirrors the AbortController pattern used by every
1148
+ // sibling bridge call (halts, judgments, recipe doctor, kill-switch).
1149
+ const controller = new AbortController();
1150
+ const timer = setTimeout(() => controller.abort(), 30_000);
1151
+ let res;
1152
+ let body;
1153
+ try {
1154
+ res = await fetch(`http://127.0.0.1:${lock.port}/recipes/run`, {
1155
+ method: "POST",
1156
+ headers: {
1157
+ Authorization: `Bearer ${lock.authToken}`,
1158
+ "Content-Type": "application/json",
1159
+ },
1160
+ body: JSON.stringify({
1161
+ name: recipeArg,
1162
+ ...(seedVars ? { vars: seedVars } : {}),
1163
+ }),
1164
+ signal: controller.signal,
1165
+ });
1166
+ body = (await res.json());
1167
+ }
1168
+ catch (err) {
1169
+ const aborted = err instanceof Error &&
1170
+ (err.name === "AbortError" || controller.signal.aborted);
1171
+ process.stderr.write(aborted
1172
+ ? `Error: bridge on port ${lock.port} did not respond within 30s — it may be wedged. Restart the bridge or re-run with --local.\n`
1173
+ : `Error: failed to reach bridge on port ${lock.port}: ${err instanceof Error ? err.message : String(err)}\n`);
1174
+ process.exit(1);
1175
+ return;
1176
+ }
1177
+ finally {
1178
+ clearTimeout(timer);
1179
+ }
1053
1180
  if (!body.ok) {
1054
1181
  // Fall through to local YAML runner if bridge doesn't know the recipe.
1055
1182
  if (!(body.error ?? "").includes("not found")) {
@@ -1080,9 +1207,37 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
1080
1207
  ? ` Running step "${step}" from recipe "${recipeArg}" locally…\n`
1081
1208
  : ` Running recipe "${recipeArg}" locally…\n`);
1082
1209
  const workdir = lock?.workspace || process.cwd();
1210
+ // PR5c — resume support: when --attempt is given, mint or reuse a
1211
+ // stable id and point the runner at a disk-backed effect ledger.
1212
+ // `--attempt new` always mints a fresh id; any other value is
1213
+ // taken verbatim (so the user can re-run the same attempt and
1214
+ // skip already-completed write tools).
1215
+ let resolvedAttempt;
1216
+ let resolvedLedgerDir;
1217
+ if (attemptId !== undefined) {
1218
+ resolvedAttempt =
1219
+ attemptId === "new"
1220
+ ? `mr_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
1221
+ : attemptId;
1222
+ // Validate at the CLI boundary so an invalid id fails loudly
1223
+ // before any side effects run (and before it lands in the run
1224
+ // log or hashed into a ledger scope key).
1225
+ try {
1226
+ const { assertValidManualRunId } = await import("./recipes/idempotencyKey.js");
1227
+ assertValidManualRunId(resolvedAttempt);
1228
+ }
1229
+ catch (err) {
1230
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1231
+ process.exit(1);
1232
+ }
1233
+ resolvedLedgerDir = ledgerDir ?? path.join(os.homedir(), ".patchwork");
1234
+ process.stdout.write(` Attempt id: ${resolvedAttempt} (ledger: ${resolvedLedgerDir})\n`);
1235
+ }
1083
1236
  const run = await runRecipe(recipeArg, {
1084
1237
  ...(step ? { step } : {}),
1085
1238
  ...(seedVars ? { vars: seedVars } : {}),
1239
+ ...(resolvedAttempt && { manualRunId: resolvedAttempt }),
1240
+ ...(resolvedLedgerDir && { ledgerDir: resolvedLedgerDir }),
1086
1241
  workdir,
1087
1242
  });
1088
1243
  if (run.stepSelection) {
@@ -1281,6 +1436,8 @@ if (process.argv[2] === "traces" && process.argv[3] === "export") {
1281
1436
  let output;
1282
1437
  let patchworkDir;
1283
1438
  let activityDir;
1439
+ let mode;
1440
+ let passphrase;
1284
1441
  for (let i = 0; i < args.length; i++) {
1285
1442
  const a = args[i];
1286
1443
  if (a === "--output" || a === "-o") {
@@ -1295,10 +1452,30 @@ if (process.argv[2] === "traces" && process.argv[3] === "export") {
1295
1452
  activityDir = args[i + 1];
1296
1453
  i++;
1297
1454
  }
1455
+ else if (a === "--mode") {
1456
+ const m = args[i + 1];
1457
+ if (m !== "public" && m !== "keyed") {
1458
+ process.stderr.write(`Error: --mode must be "public" or "keyed" (got: ${m})\n`);
1459
+ process.exit(1);
1460
+ }
1461
+ mode = m;
1462
+ i++;
1463
+ }
1464
+ else if (a === "--passphrase") {
1465
+ passphrase = args[i + 1];
1466
+ i++;
1467
+ }
1298
1468
  else if (a === "--help" || a === "-h") {
1299
- process.stdout.write("patchwork traces export [--output <path>] [--patchwork-dir <dir>] [--activity-dir <dir>]\n\n" +
1469
+ process.stdout.write("patchwork traces export [--output <path>] [--mode public|keyed]\n" +
1470
+ " [--passphrase <phrase>]\n" +
1471
+ " [--patchwork-dir <dir>] [--activity-dir <dir>]\n\n" +
1300
1472
  "Bundles ~/.patchwork/{runs,decision_traces,commit_issue_links}.jsonl\n" +
1301
1473
  "and ~/.claude/ide/activity-*.jsonl into a single gzipped JSONL file.\n\n" +
1474
+ "Modes:\n" +
1475
+ " public (default) Plain gzip bundle (.jsonl.gz). Anyone with the file\n" +
1476
+ " can read it.\n" +
1477
+ " keyed AES-256-GCM encrypted bundle (.enc). Requires --passphrase;\n" +
1478
+ " auto-detected and decrypted by `traces import --passphrase`.\n\n" +
1302
1479
  "Output is a manifest line followed by one envelope per row:\n" +
1303
1480
  ' {"type":"manifest", ...}\n' +
1304
1481
  ' {"source":"runs", "entry":{...}}\n' +
@@ -1308,11 +1485,17 @@ if (process.argv[2] === "traces" && process.argv[3] === "export") {
1308
1485
  process.exit(0);
1309
1486
  }
1310
1487
  }
1488
+ if (mode === "keyed" && !passphrase) {
1489
+ process.stderr.write("Error: --mode keyed requires --passphrase <phrase>\n");
1490
+ process.exit(1);
1491
+ }
1311
1492
  const { runTracesExport } = await import("./commands/tracesExport.js");
1312
1493
  const result = await runTracesExport({
1313
1494
  ...(output !== undefined && { output }),
1314
1495
  ...(patchworkDir !== undefined && { patchworkDir }),
1315
1496
  ...(activityDir !== undefined && { activityDir }),
1497
+ ...(mode !== undefined && { mode }),
1498
+ ...(passphrase !== undefined && { passphrase }),
1316
1499
  });
1317
1500
  process.stdout.write(` ✓ Wrote ${result.outputPath}\n`);
1318
1501
  process.stdout.write(` ${result.totalCount} rows from ${result.files.length} file${result.files.length === 1 ? "" : "s"} (${result.totalBytes} bytes read)\n`);
@@ -1402,8 +1585,9 @@ if (process.argv[2] === "traces" && process.argv[3] === "import") {
1402
1585
  }
1403
1586
  const plain = decryptTraceBundle(raw, passphrase);
1404
1587
  const { tmpdir } = await import("node:os");
1588
+ const { join: pathJoin } = await import("node:path");
1405
1589
  const { writeFileSync } = await import("node:fs");
1406
- const tmp = `${tmpdir()}/patchwork-import-${Date.now()}.jsonl.gz`;
1590
+ const tmp = pathJoin(tmpdir(), `patchwork-import-${Date.now()}.jsonl.gz`);
1407
1591
  writeFileSync(tmp, plain, { mode: 0o600 });
1408
1592
  input = tmp;
1409
1593
  process.stderr.write("Decryption succeeded.\n");
@@ -1435,6 +1619,527 @@ if (process.argv[2] === "traces" && process.argv[3] === "import") {
1435
1619
  }
1436
1620
  })();
1437
1621
  }
1622
+ // `patchwork kill-switch engage|release|status` — issue #422 step 3.
1623
+ //
1624
+ // Discovers the running bridge via lock file, POSTs /kill-switch with
1625
+ // Bearer auth, and surfaces structured errors (env-locked, no-bridge,
1626
+ // wedged-bridge). Multi-bridge fan-out: iterates ALL live `isBridge:true`
1627
+ // locks and engages/releases each (v2-B2 from #422).
1628
+ //
1629
+ // v2-I4: mandatory 10s deadline per request. No silent fallback on
1630
+ // timeout/ECONNREFUSED/non-2xx — error message + exit non-zero.
1631
+ if (process.argv[2] === "kill-switch") {
1632
+ const sub = process.argv[3];
1633
+ if (!sub || (sub !== "engage" && sub !== "release" && sub !== "status")) {
1634
+ process.stderr.write('Usage: patchwork kill-switch <engage|release|status> [--reason "..."]\n' +
1635
+ "\n" +
1636
+ " engage Block all write-tier tool calls across every running bridge.\n" +
1637
+ " release Resume writes.\n" +
1638
+ " status Print engaged/locked state per running bridge.\n" +
1639
+ "\n" +
1640
+ "Exits non-zero if any bridge is unreachable or env-locked.\n");
1641
+ process.exit(1);
1642
+ }
1643
+ (async () => {
1644
+ try {
1645
+ // Parse optional flags early so --force-local can be used without a bridge.
1646
+ const args = process.argv.slice(4);
1647
+ const reasonIdx = args.findIndex((a) => a === "--reason" || a === "-m");
1648
+ const reason = reasonIdx >= 0 && reasonIdx + 1 < args.length
1649
+ ? args[reasonIdx + 1]
1650
+ : undefined;
1651
+ // v2-I4: --force-local writes flags.json directly when no live bridge
1652
+ // is reachable. The running bridge's fs.watch (v2-S1) picks up the
1653
+ // change within ~100ms; without a running bridge this is "effective
1654
+ // next boot" — which is still better than a silent noop.
1655
+ const forceLocal = args.includes("--force-local");
1656
+ // v2-B2: enumerate ALL live bridge locks (not just the first).
1657
+ const { findAllLiveBridges } = await import("./bridgeLockDiscovery.js");
1658
+ const liveLocks = findAllLiveBridges();
1659
+ if (liveLocks.length === 0) {
1660
+ if (forceLocal && (sub === "engage" || sub === "release")) {
1661
+ // --force-local: write flags.json directly. The running bridge's
1662
+ // fs.watch picks this up within ~100ms; if the bridge is wedged
1663
+ // or not started, this is effective on next start.
1664
+ const { setFlag, KILL_SWITCH_WRITES } = await import("./featureFlags.js");
1665
+ const engage = sub === "engage";
1666
+ setFlag(KILL_SWITCH_WRITES, engage, true);
1667
+ // Audit in a sibling CLI-only JSONL (v2-I10: bridge-only writes
1668
+ // go to decision_traces.jsonl; CLI fallback is distinct).
1669
+ const os = await import("node:os");
1670
+ const path = await import("node:path");
1671
+ const fs = await import("node:fs");
1672
+ const cliTraceFile = path.join(process.env.PATCHWORK_HOME ??
1673
+ path.join(os.default.homedir(), ".patchwork"), "decision_traces.cli.jsonl");
1674
+ const dir = path.dirname(cliTraceFile);
1675
+ if (!fs.existsSync(dir))
1676
+ fs.mkdirSync(dir, { recursive: true });
1677
+ const entry = JSON.stringify({
1678
+ ts: new Date().toISOString(),
1679
+ event: engage ? "engage" : "release",
1680
+ actor: "cli-force-local",
1681
+ ...(reason ? { reason } : {}),
1682
+ });
1683
+ fs.appendFileSync(cliTraceFile, `${entry}\n`);
1684
+ process.stdout.write(` ✓ kill-switch ${engage ? "ENGAGED" : "released"} via --force-local (flags.json written directly).\n` +
1685
+ " Running bridges will pick this up via fs.watch within ~100ms.\n");
1686
+ process.exit(0);
1687
+ }
1688
+ process.stderr.write("No running bridge found.\n" +
1689
+ " - For `engage`/`release`, kill-switch has no live target to update.\n" +
1690
+ " - Use --force-local to write flags.json directly (bridge fs.watch picks it up).\n" +
1691
+ " - Or restart the bridge and re-run this command.\n");
1692
+ process.exit(2);
1693
+ }
1694
+ // v2-I4: 10s per-request deadline. AbortController per call.
1695
+ async function callBridge(lock, method, body) {
1696
+ const controller = new AbortController();
1697
+ const timer = setTimeout(() => controller.abort(), 10_000);
1698
+ try {
1699
+ const res = await fetch(`http://127.0.0.1:${lock.port}/kill-switch`, {
1700
+ method,
1701
+ headers: {
1702
+ Authorization: `Bearer ${lock.authToken}`,
1703
+ "Content-Type": "application/json",
1704
+ },
1705
+ ...(body ? { body: JSON.stringify(body) } : {}),
1706
+ signal: controller.signal,
1707
+ });
1708
+ let json;
1709
+ try {
1710
+ json = (await res.json());
1711
+ }
1712
+ catch {
1713
+ json = undefined;
1714
+ }
1715
+ return {
1716
+ ok: res.status >= 200 && res.status < 300,
1717
+ status: res.status,
1718
+ ...(json ? { json } : {}),
1719
+ };
1720
+ }
1721
+ catch (err) {
1722
+ return {
1723
+ ok: false,
1724
+ status: 0,
1725
+ error: err instanceof Error ? err.message : String(err),
1726
+ };
1727
+ }
1728
+ finally {
1729
+ clearTimeout(timer);
1730
+ }
1731
+ }
1732
+ if (sub === "status") {
1733
+ let anyFailed = false;
1734
+ for (const lock of liveLocks) {
1735
+ const result = await callBridge(lock, "GET");
1736
+ if (!result.ok) {
1737
+ anyFailed = true;
1738
+ process.stderr.write(` ✗ bridge pid=${lock.pid} port=${lock.port} unreachable (${result.error ?? `status ${result.status}`})\n`);
1739
+ continue;
1740
+ }
1741
+ const j = result.json ?? {};
1742
+ const engaged = j.engaged === true ? "ENGAGED" : "released";
1743
+ const lockedSuffix = j.locked
1744
+ ? ` [env-locked: ${j.lockedReason ?? "yes"}]`
1745
+ : "";
1746
+ const wsLabel = lock.workspace
1747
+ ? lock.workspace.split("/").slice(-2).join("/")
1748
+ : `pid=${lock.pid}`;
1749
+ process.stdout.write(` ${engaged} port=${lock.port} ${wsLabel}${lockedSuffix}\n`);
1750
+ }
1751
+ process.exit(anyFailed ? 2 : 0);
1752
+ }
1753
+ // engage / release: POST to every live bridge, surface aggregate result.
1754
+ const engage = sub === "engage";
1755
+ let anyFailed = false;
1756
+ let anyChanged = false;
1757
+ for (const lock of liveLocks) {
1758
+ const result = await callBridge(lock, "POST", {
1759
+ engage,
1760
+ ...(reason ? { reason } : {}),
1761
+ });
1762
+ const wsLabel = lock.workspace
1763
+ ? lock.workspace.split("/").slice(-2).join("/")
1764
+ : `pid=${lock.pid}`;
1765
+ if (result.status === 409) {
1766
+ anyFailed = true;
1767
+ const lr = result.json?.lockedReason ??
1768
+ "env-locked at boot";
1769
+ process.stderr.write(` ✗ port=${lock.port} ${wsLabel}: cannot ${sub} — ${lr}\n`);
1770
+ continue;
1771
+ }
1772
+ if (!result.ok) {
1773
+ anyFailed = true;
1774
+ process.stderr.write(` ✗ port=${lock.port} ${wsLabel}: ${result.error ?? `status ${result.status}`}\n`);
1775
+ continue;
1776
+ }
1777
+ const j = result.json ?? {};
1778
+ const changedTag = j.changed === true ? "" : " (no-op, already in state)";
1779
+ if (j.changed === true)
1780
+ anyChanged = true;
1781
+ process.stdout.write(` ✓ port=${lock.port} ${wsLabel}: ${engage ? "ENGAGED" : "released"}${changedTag}\n`);
1782
+ }
1783
+ if (anyFailed) {
1784
+ process.exit(2);
1785
+ }
1786
+ if (!anyChanged) {
1787
+ process.stdout.write(`\n All ${liveLocks.length} bridge${liveLocks.length === 1 ? "" : "s"} already in target state — no audit emit.\n`);
1788
+ }
1789
+ else {
1790
+ process.stdout.write(`\n Kill-switch ${engage ? "engaged" : "released"} on ${liveLocks.length} bridge${liveLocks.length === 1 ? "" : "s"}.\n`);
1791
+ }
1792
+ process.exit(0);
1793
+ }
1794
+ catch (err) {
1795
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1796
+ process.exit(1);
1797
+ }
1798
+ })();
1799
+ }
1800
+ // `patchwork panic` — alias for `patchwork kill-switch engage` (v2-Strong-2).
1801
+ //
1802
+ // Discoverable under stress (short command, obvious intent). Canonical noun
1803
+ // form is `kill-switch engage`; this alias matches it so shell history six
1804
+ // months later still makes sense. Does not accept sub-verbs — just runs engage.
1805
+ if (process.argv[2] === "panic") {
1806
+ const extra = process.argv.slice(3); // e.g. --reason "..." --force-local
1807
+ // Guard against `panic --help` engaging the kill switch — a real
1808
+ // footgun if you tab-completed the verb to confirm syntax before
1809
+ // committing to the action. `panic` is an alias, so we honor --help
1810
+ // here ourselves rather than forwarding to kill-switch engage.
1811
+ if (extra.includes("--help") || extra.includes("-h")) {
1812
+ console.log('Usage: patchwork panic [--reason "..."] [--force-local]\n\n' +
1813
+ " Alias for `patchwork kill-switch engage` — blocks all write-tier\n" +
1814
+ " tool calls across every running bridge. Use --reason to leave a\n" +
1815
+ " note in the audit trail. Release with `patchwork kill-switch release`.\n");
1816
+ process.exit(0);
1817
+ }
1818
+ // Spawn self with kill-switch engage to reuse the full handler without
1819
+ // duplicating 200+ LOC. Passes through any flags (--reason, --force-local).
1820
+ import("node:child_process").then(({ spawnSync }) => {
1821
+ const self = process.argv[1] ?? process.execPath;
1822
+ const result = spawnSync(process.execPath, [self, "kill-switch", "engage", ...extra], { stdio: "inherit" });
1823
+ process.exit(result.status ?? 1);
1824
+ });
1825
+ }
1826
+ // `patchwork halts` — one-screen morning summary of recent recipe halts.
1827
+ //
1828
+ // Composes the haltReason field (#441), category aggregator + endpoint
1829
+ // (#444), and dashboard pill conventions: queries the live bridge's
1830
+ // /runs/halt-summary endpoint over the chosen window and prints a
1831
+ // per-category breakdown plus the 5 most-recent halt reasons. Default
1832
+ // window is "overnight" (since 6pm yesterday local) so it lines up with
1833
+ // "what halted while I was asleep?".
1834
+ if (process.argv[2] === "halts") {
1835
+ const args = process.argv.slice(3);
1836
+ const wantHelp = args.includes("--help") || args.includes("-h");
1837
+ if (wantHelp) {
1838
+ process.stdout.write("Usage: patchwork halts [--window <name>] [--recipe <name>] [--json]\n" +
1839
+ "\n" +
1840
+ " --window 1h | 24h | overnight | 7d | any (default: overnight)\n" +
1841
+ " --recipe <name> filter to one recipe by name\n" +
1842
+ " --json emit raw JSON (for scripting)\n" +
1843
+ "\n" +
1844
+ '"overnight" = since 6pm yesterday local time.\n');
1845
+ process.exit(0);
1846
+ }
1847
+ function parseWindow() {
1848
+ const idx = args.findIndex((a) => a === "--window" || a === "-w");
1849
+ const raw = idx >= 0 && idx + 1 < args.length ? args[idx + 1] : "overnight";
1850
+ if (raw === "1h" ||
1851
+ raw === "24h" ||
1852
+ raw === "overnight" ||
1853
+ raw === "7d" ||
1854
+ raw === "any")
1855
+ return raw;
1856
+ process.stderr.write(`Unknown --window value: "${raw}"\n`);
1857
+ process.exit(1);
1858
+ }
1859
+ function windowSinceMs(w) {
1860
+ if (w === "any")
1861
+ return null;
1862
+ if (w === "1h")
1863
+ return 60 * 60 * 1000;
1864
+ if (w === "24h")
1865
+ return 24 * 60 * 60 * 1000;
1866
+ if (w === "7d")
1867
+ return 7 * 24 * 60 * 60 * 1000;
1868
+ const d = new Date();
1869
+ d.setHours(18, 0, 0, 0);
1870
+ if (d.getTime() > Date.now())
1871
+ d.setDate(d.getDate() - 1);
1872
+ return Date.now() - d.getTime();
1873
+ }
1874
+ const window = parseWindow();
1875
+ const wantJson = args.includes("--json");
1876
+ const recipeIdx = args.findIndex((a) => a === "--recipe" || a === "-r");
1877
+ const recipeFilter = recipeIdx >= 0 && recipeIdx + 1 < args.length
1878
+ ? args[recipeIdx + 1]
1879
+ : undefined;
1880
+ (async () => {
1881
+ try {
1882
+ const { findAllLiveBridges } = await import("./bridgeLockDiscovery.js");
1883
+ const liveLocks = findAllLiveBridges();
1884
+ if (liveLocks.length === 0) {
1885
+ process.stderr.write("No running bridge found. Start one with `patchwork start` (or `--driver subprocess`).\n");
1886
+ process.exit(2);
1887
+ }
1888
+ const sinceMs = windowSinceMs(window);
1889
+ const params = [];
1890
+ if (sinceMs != null)
1891
+ params.push(`sinceMs=${sinceMs}`);
1892
+ if (recipeFilter)
1893
+ params.push(`recipe=${encodeURIComponent(recipeFilter)}`);
1894
+ const qs = params.length > 0 ? `?${params.join("&")}` : "";
1895
+ // Walk live bridges in order; first responsive one wins. See the
1896
+ // matching block in the `judgments` handler — findAllLiveBridges
1897
+ // can include stale entries when a recycled PID still answers
1898
+ // `kill(pid, 0)` but the lock points at a dead bridge.
1899
+ let res = null;
1900
+ let lastStatus = 0;
1901
+ for (const lock of liveLocks) {
1902
+ const controller = new AbortController();
1903
+ const timer = setTimeout(() => controller.abort(), 10_000);
1904
+ try {
1905
+ const candidate = await fetch(`http://127.0.0.1:${lock.port}/runs/halt-summary${qs}`, {
1906
+ headers: { Authorization: `Bearer ${lock.authToken}` },
1907
+ signal: controller.signal,
1908
+ });
1909
+ if (candidate.ok) {
1910
+ res = candidate;
1911
+ break;
1912
+ }
1913
+ lastStatus = candidate.status;
1914
+ }
1915
+ catch {
1916
+ /* unreachable lock — try next */
1917
+ }
1918
+ finally {
1919
+ clearTimeout(timer);
1920
+ }
1921
+ }
1922
+ if (!res) {
1923
+ process.stderr.write(`No live bridge served /runs/halt-summary (last status: ${lastStatus || "unreachable"}).\n`);
1924
+ process.exit(1);
1925
+ }
1926
+ const summary = (await res.json());
1927
+ if (wantJson) {
1928
+ process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
1929
+ process.exit(0);
1930
+ }
1931
+ // Shared label + hint maps (also used by `recipe doctor` and
1932
+ // mirrored in the dashboard) so the wording stays consistent.
1933
+ const { HALT_CATEGORY_LABELS, HALT_CATEGORY_HINTS } = await import("./recipes/haltCategory.js");
1934
+ const windowLabel = {
1935
+ "1h": "last hour",
1936
+ "24h": "last 24h",
1937
+ overnight: "since 6pm yesterday",
1938
+ "7d": "last 7 days",
1939
+ any: "all time",
1940
+ };
1941
+ const recipeSuffix = recipeFilter ? ` · recipe="${recipeFilter}"` : "";
1942
+ process.stdout.write(`Halts — ${windowLabel[window]}${recipeSuffix}\n`);
1943
+ process.stdout.write(`Total: ${summary.total}\n`);
1944
+ if (summary.total === 0) {
1945
+ process.stdout.write("\n (nothing halted in this window)\n");
1946
+ process.exit(0);
1947
+ }
1948
+ const entries = Object.entries(summary.byCategory).sort(([, a], [, b]) => b - a);
1949
+ process.stdout.write("\nBy category:\n");
1950
+ for (const [cat, count] of entries) {
1951
+ const label = HALT_CATEGORY_LABELS[cat] ?? cat;
1952
+ const hint = HALT_CATEGORY_HINTS[cat];
1953
+ const hintSuffix = hint ? ` — ${hint}` : "";
1954
+ process.stdout.write(` ${String(count).padStart(3)} ${label}${hintSuffix}\n`);
1955
+ }
1956
+ if (summary.recent.length > 0) {
1957
+ process.stdout.write("\nMost recent:\n");
1958
+ for (const r of summary.recent) {
1959
+ // Truncate the reason to ~120 chars so a wide stack trace
1960
+ // can't blow up the terminal width on phones / narrow panes.
1961
+ const reason = r.reason.length > 120 ? `${r.reason.slice(0, 117)}…` : r.reason;
1962
+ process.stdout.write(` #${r.runSeq} [${r.category}] ${reason}\n`);
1963
+ }
1964
+ }
1965
+ process.exit(0);
1966
+ }
1967
+ catch (err) {
1968
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1969
+ process.exit(1);
1970
+ }
1971
+ })();
1972
+ }
1973
+ // `patchwork connect` — connector front-door. Thin CLI→HTTP shim over the
1974
+ // bridge's `/connections/*` routes (list / OAuth auth / PAT connect / test /
1975
+ // disconnect). This is the command the 23 connector "Run: patchwork connect
1976
+ // <vendor>" error messages point at. Lock discovery reuses the same
1977
+ // isBridge:true + live-PID selector as `recipe run`, `quick-task`, etc.
1978
+ if (process.argv[2] === "connect") {
1979
+ (async () => {
1980
+ const { runConnect } = await import("./commands/connect.js");
1981
+ const { findBridgeLockForTask } = await import("./commands/task.js");
1982
+ await runConnect(process.argv.slice(3), {
1983
+ findBridgeLock: (port) => findBridgeLockForTask(port),
1984
+ });
1985
+ })();
1986
+ }
1987
+ // `patchwork analytics` — manage the self-hosted telemetry collector config.
1988
+ // Replaces the brittle "endpoint+secret in launchd plist" pattern with a
1989
+ // proper config file the bridge reads at startup.
1990
+ if (process.argv[2] === "analytics") {
1991
+ (async () => {
1992
+ const { runAnalyticsCommand } = await import("./commands/analytics.js");
1993
+ const code = await runAnalyticsCommand(process.argv.slice(3));
1994
+ process.exit(code);
1995
+ })();
1996
+ }
1997
+ // `patchwork judgments` — PR3b sibling of `patchwork halts`. Same window
1998
+ // + recipe filter shape; queries /runs/judge-summary and prints a
1999
+ // per-verdict breakdown plus the 5 most-recent verdicts.
2000
+ if (process.argv[2] === "judgments") {
2001
+ const args = process.argv.slice(3);
2002
+ const wantHelp = args.includes("--help") || args.includes("-h");
2003
+ if (wantHelp) {
2004
+ process.stdout.write("Usage: patchwork judgments [--window <name>] [--recipe <name>] [--json]\n" +
2005
+ "\n" +
2006
+ " --window 1h | 24h | overnight | 7d | any (default: overnight)\n" +
2007
+ " --recipe <name> filter to one recipe by name\n" +
2008
+ " --json emit raw JSON (for scripting)\n" +
2009
+ "\n" +
2010
+ '"overnight" = since 6pm yesterday local time.\n');
2011
+ process.exit(0);
2012
+ }
2013
+ function parseWindow() {
2014
+ const idx = args.findIndex((a) => a === "--window" || a === "-w");
2015
+ const raw = idx >= 0 && idx + 1 < args.length ? args[idx + 1] : "overnight";
2016
+ if (raw === "1h" ||
2017
+ raw === "24h" ||
2018
+ raw === "overnight" ||
2019
+ raw === "7d" ||
2020
+ raw === "any")
2021
+ return raw;
2022
+ process.stderr.write(`Unknown --window value: "${raw}"\n`);
2023
+ process.exit(1);
2024
+ }
2025
+ function windowSinceMs(w) {
2026
+ if (w === "any")
2027
+ return null;
2028
+ if (w === "1h")
2029
+ return 60 * 60 * 1000;
2030
+ if (w === "24h")
2031
+ return 24 * 60 * 60 * 1000;
2032
+ if (w === "7d")
2033
+ return 7 * 24 * 60 * 60 * 1000;
2034
+ const d = new Date();
2035
+ d.setHours(18, 0, 0, 0);
2036
+ if (d.getTime() > Date.now())
2037
+ d.setDate(d.getDate() - 1);
2038
+ return Date.now() - d.getTime();
2039
+ }
2040
+ const window = parseWindow();
2041
+ const wantJson = args.includes("--json");
2042
+ const recipeIdx = args.findIndex((a) => a === "--recipe" || a === "-r");
2043
+ const recipeFilter = recipeIdx >= 0 && recipeIdx + 1 < args.length
2044
+ ? args[recipeIdx + 1]
2045
+ : undefined;
2046
+ (async () => {
2047
+ try {
2048
+ const { findAllLiveBridges } = await import("./bridgeLockDiscovery.js");
2049
+ const liveLocks = findAllLiveBridges();
2050
+ if (liveLocks.length === 0) {
2051
+ process.stderr.write("No running bridge found. Start one with `patchwork start` (or `--driver subprocess`).\n");
2052
+ process.exit(2);
2053
+ }
2054
+ const sinceMs = windowSinceMs(window);
2055
+ const params = [];
2056
+ if (sinceMs != null)
2057
+ params.push(`sinceMs=${sinceMs}`);
2058
+ if (recipeFilter)
2059
+ params.push(`recipe=${encodeURIComponent(recipeFilter)}`);
2060
+ const qs = params.length > 0 ? `?${params.join("&")}` : "";
2061
+ // Walk live bridges in order; the first responsive one wins.
2062
+ // findAllLiveBridges uses `kill(pid, 0)` for liveness, which
2063
+ // returns true for any recycled PID — so liveLocks can contain
2064
+ // stale entries from dead bridges. Previously we picked [0]
2065
+ // unconditionally and surfaced a confusing 404; now we try each
2066
+ // and only fall through to the error path when *all* fail.
2067
+ let res = null;
2068
+ let lastStatus = 0;
2069
+ for (const lock of liveLocks) {
2070
+ const controller = new AbortController();
2071
+ const timer = setTimeout(() => controller.abort(), 10_000);
2072
+ try {
2073
+ const candidate = await fetch(`http://127.0.0.1:${lock.port}/runs/judge-summary${qs}`, {
2074
+ headers: { Authorization: `Bearer ${lock.authToken}` },
2075
+ signal: controller.signal,
2076
+ });
2077
+ if (candidate.ok) {
2078
+ res = candidate;
2079
+ break;
2080
+ }
2081
+ lastStatus = candidate.status;
2082
+ }
2083
+ catch {
2084
+ /* unreachable lock — try next */
2085
+ }
2086
+ finally {
2087
+ clearTimeout(timer);
2088
+ }
2089
+ }
2090
+ if (!res) {
2091
+ process.stderr.write(`No live bridge served /runs/judge-summary (last status: ${lastStatus || "unreachable"}).\n`);
2092
+ process.exit(1);
2093
+ }
2094
+ const summary = (await res.json());
2095
+ if (wantJson) {
2096
+ process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
2097
+ process.exit(0);
2098
+ }
2099
+ const labels = {
2100
+ approve: "approve",
2101
+ request_changes: "request changes",
2102
+ unparseable: "unparseable",
2103
+ };
2104
+ const windowLabel = {
2105
+ "1h": "last hour",
2106
+ "24h": "last 24h",
2107
+ overnight: "since 6pm yesterday",
2108
+ "7d": "last 7 days",
2109
+ any: "all time",
2110
+ };
2111
+ const recipeSuffix = recipeFilter ? ` · recipe="${recipeFilter}"` : "";
2112
+ process.stdout.write(`Judgments — ${windowLabel[window]}${recipeSuffix}\n`);
2113
+ process.stdout.write(`Total: ${summary.total}\n`);
2114
+ if (summary.total === 0) {
2115
+ process.stdout.write("\n (no judge steps fired in this window)\n");
2116
+ process.exit(0);
2117
+ }
2118
+ const entries = Object.entries(summary.byVerdict).sort(([, a], [, b]) => b - a);
2119
+ process.stdout.write("\nBy verdict:\n");
2120
+ for (const [verdict, count] of entries) {
2121
+ const label = labels[verdict] ?? verdict;
2122
+ process.stdout.write(` ${String(count).padStart(3)} ${label}\n`);
2123
+ }
2124
+ if (summary.recent.length > 0) {
2125
+ process.stdout.write("\nMost recent:\n");
2126
+ for (const r of summary.recent) {
2127
+ const reason = r.firstReason
2128
+ ? r.firstReason.length > 120
2129
+ ? `${r.firstReason.slice(0, 117)}…`
2130
+ : r.firstReason
2131
+ : "(no reason)";
2132
+ process.stdout.write(` #${r.runSeq} [${r.verdict}] ${r.stepId}: ${reason}\n`);
2133
+ }
2134
+ }
2135
+ process.exit(0);
2136
+ }
2137
+ catch (err) {
2138
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
2139
+ process.exit(1);
2140
+ }
2141
+ })();
2142
+ }
1438
2143
  if (process.argv[2] === "recipe" && process.argv[3] === "schema") {
1439
2144
  const outputDir = process.argv[4] ?? path.join(process.cwd(), "schemas");
1440
2145
  (async () => {
@@ -1454,44 +2159,62 @@ if (process.argv[2] === "recipe" && process.argv[3] === "schema") {
1454
2159
  })();
1455
2160
  }
1456
2161
  // Patchwork: `patchwork recipe new <name>` — scaffold a new recipe from template.
2162
+ // With `--interactive`, drops into a connector-aware prompt tree instead.
1457
2163
  if (process.argv[2] === "recipe" && process.argv[3] === "new") {
1458
2164
  const args = process.argv.slice(4);
1459
- const recipeName = args[0];
1460
- if (!recipeName) {
1461
- process.stderr.write("Usage: patchwork recipe new <name> [--template <name>] [--desc <description>] [--out <dir>]\n" +
1462
- " --out <dir> Write the recipe to <dir>/<name>.yaml.\n" +
1463
- " Defaults to ~/.patchwork/recipes/ — pass `--out .` to\n" +
1464
- " write into the current directory instead.\n");
1465
- process.stderr.write("\nTemplates:\n");
1466
- (async () => {
1467
- const { listTemplates } = await import("./commands/recipe.js");
1468
- for (const t of listTemplates()) {
1469
- process.stderr.write(` ${t}\n`);
1470
- }
1471
- process.exit(1);
1472
- })();
1473
- }
1474
- else {
2165
+ const isInteractive = args.includes("--interactive") || args.includes("-i");
2166
+ if (isInteractive) {
1475
2167
  (async () => {
1476
2168
  try {
1477
- const { runNew } = await import("./commands/recipe.js");
1478
- const templateIdx = args.indexOf("--template");
1479
- const template = templateIdx >= 0 ? args[templateIdx + 1] : undefined;
1480
- const descIdx = args.indexOf("--desc");
1481
- const description = (descIdx >= 0 ? args[descIdx + 1] : undefined) ??
1482
- `Recipe: ${recipeName}`;
2169
+ const { runNewInteractive } = await import("./commands/recipe.js");
2170
+ const { createInterface } = await import("node:readline/promises");
2171
+ const rl = createInterface({
2172
+ input: process.stdin,
2173
+ output: process.stdout,
2174
+ });
1483
2175
  const outIdx = args.indexOf("--out");
1484
2176
  const outRaw = outIdx >= 0 ? args[outIdx + 1] : undefined;
1485
- // `--out .` is the common case for "scaffold in cwd" — resolve so
1486
- // the success message shows the absolute path the user can open.
1487
2177
  const outputDir = outRaw ? path.resolve(outRaw) : undefined;
1488
- const result = runNew({
1489
- name: recipeName,
1490
- description,
1491
- ...(template ? { template } : {}),
2178
+ const deps = {
2179
+ ask: async (q) => (await rl.question(`${q}: `)).trim(),
2180
+ pickFromList: async (q, options) => {
2181
+ process.stdout.write(`\n${q}\n`);
2182
+ options.forEach((opt, i) => {
2183
+ process.stdout.write(` ${i + 1}. ${opt}\n`);
2184
+ });
2185
+ for (let attempt = 0; attempt < 5; attempt++) {
2186
+ const raw = (await rl.question(`Choose 1-${options.length}: `)).trim();
2187
+ const idx = Number.parseInt(raw, 10);
2188
+ if (Number.isFinite(idx) && idx >= 1 && idx <= options.length) {
2189
+ return idx;
2190
+ }
2191
+ process.stdout.write(`Invalid choice. Enter a number 1-${options.length}.\n`);
2192
+ }
2193
+ throw new Error("Too many invalid choices");
2194
+ },
2195
+ confirm: async (q) => {
2196
+ const a = (await rl.question(`${q} [y/N]: `)).trim().toLowerCase();
2197
+ return a === "y" || a === "yes";
2198
+ },
2199
+ preview: (yaml) => {
2200
+ process.stdout.write("\n--- Preview ---\n");
2201
+ process.stdout.write(yaml);
2202
+ process.stdout.write("---\n\n");
2203
+ },
2204
+ };
2205
+ const result = await runNewInteractive({
2206
+ deps,
1492
2207
  ...(outputDir ? { outputDir } : {}),
1493
2208
  });
2209
+ rl.close();
1494
2210
  process.stdout.write(` ✓ Created ${result.path}\n`);
2211
+ if (result.warnings.length > 0) {
2212
+ process.stdout.write(`\n ⚠ Lint warnings (recipe still written):\n`);
2213
+ for (const w of result.warnings) {
2214
+ process.stdout.write(` [${w.level}] ${w.message}\n`);
2215
+ }
2216
+ }
2217
+ process.stdout.write(`\n Run with: patchwork recipe run ${result.path}\n`);
1495
2218
  process.exit(0);
1496
2219
  }
1497
2220
  catch (err) {
@@ -1500,6 +2223,53 @@ if (process.argv[2] === "recipe" && process.argv[3] === "new") {
1500
2223
  }
1501
2224
  })();
1502
2225
  }
2226
+ else {
2227
+ const recipeName = args[0];
2228
+ if (!recipeName) {
2229
+ process.stderr.write("Usage: patchwork recipe new <name> [--template <name>] [--desc <description>] [--out <dir>]\n" +
2230
+ " --interactive (-i) Run the connector-aware prompt tree instead of using a template.\n" +
2231
+ " --out <dir> Write the recipe to <dir>/<name>.yaml.\n" +
2232
+ " Defaults to ~/.patchwork/recipes/ — pass `--out .` to\n" +
2233
+ " write into the current directory instead.\n");
2234
+ process.stderr.write("\nTemplates:\n");
2235
+ (async () => {
2236
+ const { listTemplates } = await import("./commands/recipe.js");
2237
+ for (const t of listTemplates()) {
2238
+ process.stderr.write(` ${t}\n`);
2239
+ }
2240
+ process.exit(1);
2241
+ })();
2242
+ }
2243
+ else {
2244
+ (async () => {
2245
+ try {
2246
+ const { runNew } = await import("./commands/recipe.js");
2247
+ const templateIdx = args.indexOf("--template");
2248
+ const template = templateIdx >= 0 ? args[templateIdx + 1] : undefined;
2249
+ const descIdx = args.indexOf("--desc");
2250
+ const description = (descIdx >= 0 ? args[descIdx + 1] : undefined) ??
2251
+ `Recipe: ${recipeName}`;
2252
+ const outIdx = args.indexOf("--out");
2253
+ const outRaw = outIdx >= 0 ? args[outIdx + 1] : undefined;
2254
+ // `--out .` is the common case for "scaffold in cwd" — resolve so
2255
+ // the success message shows the absolute path the user can open.
2256
+ const outputDir = outRaw ? path.resolve(outRaw) : undefined;
2257
+ const result = runNew({
2258
+ name: recipeName,
2259
+ description,
2260
+ ...(template ? { template } : {}),
2261
+ ...(outputDir ? { outputDir } : {}),
2262
+ });
2263
+ process.stdout.write(` ✓ Created ${result.path}\n`);
2264
+ process.exit(0);
2265
+ }
2266
+ catch (err) {
2267
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
2268
+ process.exit(1);
2269
+ }
2270
+ })();
2271
+ }
2272
+ }
1503
2273
  }
1504
2274
  // Patchwork: `patchwork recipe lint <file.yaml>` — validate recipe against schema.
1505
2275
  if (process.argv[2] === "recipe" && process.argv[3] === "lint") {
@@ -1531,6 +2301,53 @@ if (process.argv[2] === "recipe" && process.argv[3] === "lint") {
1531
2301
  }
1532
2302
  })();
1533
2303
  }
2304
+ // Patchwork: `patchwork recipe audit-env <recipe>` — check {{env.FOO}} references are satisfied.
2305
+ if (process.argv[2] === "recipe" && process.argv[3] === "audit-env") {
2306
+ const recipeArg = process.argv[4];
2307
+ if (!recipeArg) {
2308
+ process.stderr.write("Usage: patchwork recipe audit-env <recipe> [--env-file <path>] [--workspace <path>]\n");
2309
+ process.exit(1);
2310
+ }
2311
+ (async () => {
2312
+ try {
2313
+ const args = process.argv.slice(5);
2314
+ const envFileIdx = args.indexOf("--env-file");
2315
+ const envFile = envFileIdx !== -1 ? args[envFileIdx + 1] : undefined;
2316
+ const workspaceIdx = args.indexOf("--workspace");
2317
+ const workspace = workspaceIdx !== -1 && args[workspaceIdx + 1]
2318
+ ? args[workspaceIdx + 1]
2319
+ : process.cwd();
2320
+ const { runAuditEnv } = await import("./commands/auditEnv.js");
2321
+ const result = await runAuditEnv(recipeArg, {
2322
+ ...(envFile ? { envFile } : {}),
2323
+ workspace,
2324
+ });
2325
+ if (result.warnings.length > 0) {
2326
+ for (const w of result.warnings) {
2327
+ process.stderr.write(` ⚠ ${w}\n`);
2328
+ }
2329
+ }
2330
+ if (result.present.length > 0) {
2331
+ for (const v of result.present) {
2332
+ process.stdout.write(` ✓ ${v}\n`);
2333
+ }
2334
+ }
2335
+ if (result.missing.length > 0) {
2336
+ for (const v of result.missing) {
2337
+ process.stderr.write(` ✗ missing: ${v}\n`);
2338
+ }
2339
+ }
2340
+ if (!result.ok) {
2341
+ process.exit(1);
2342
+ }
2343
+ process.exit(0);
2344
+ }
2345
+ catch (err) {
2346
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
2347
+ process.exit(1);
2348
+ }
2349
+ })();
2350
+ }
1534
2351
  // Patchwork: `patchwork recipe preflight <file.yaml>` — static policy check (lint + plan + writes + fixtures).
1535
2352
  if (process.argv[2] === "recipe" && process.argv[3] === "preflight") {
1536
2353
  const args = process.argv.slice(4);
@@ -1634,6 +2451,114 @@ if (process.argv[2] === "recipe" && process.argv[3] === "preflight") {
1634
2451
  }
1635
2452
  })();
1636
2453
  }
2454
+ // `recipe doctor <name|file>` — one-screen "why is this recipe unhealthy
2455
+ // + how do I fix it" diagnosis. Composes the static preflight check with
2456
+ // the recipe-scoped runtime halt summary from a live bridge (fail-soft:
2457
+ // no bridge → static-only). See runRecipeDoctor in commands/recipe.ts.
2458
+ if (process.argv[2] === "recipe" && process.argv[3] === "doctor") {
2459
+ const args = process.argv.slice(4);
2460
+ const usage = "Usage: patchwork recipe doctor <name|file.yaml> [--json] [--local]\n\n" +
2461
+ "Diagnoses a recipe: lint + write-policy + plan (static) plus recent\n" +
2462
+ "runtime halts from a live bridge, each mapped to a fix hint.\n" +
2463
+ "--local skips the bridge runtime check. Exits 1 when unhealthy.\n";
2464
+ if (args.includes("--help") || args.includes("-h")) {
2465
+ process.stdout.write(usage);
2466
+ process.exit(0);
2467
+ }
2468
+ const wantJson = args.includes("--json");
2469
+ const localOnly = args.includes("--local");
2470
+ const ref = args.find((a) => !a.startsWith("--"));
2471
+ if (!ref) {
2472
+ process.stderr.write(usage);
2473
+ process.exit(1);
2474
+ }
2475
+ (async () => {
2476
+ try {
2477
+ const { runRecipeDoctor, formatRecipeDoctorReport } = await import("./commands/recipe.js");
2478
+ // Runtime halt fetcher: walk live bridges, query the recipe-scoped
2479
+ // halt summary over the last 7 days. Returns null when no bridge is
2480
+ // reachable so doctor degrades to static-only.
2481
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
2482
+ const fetchHalts = localOnly
2483
+ ? undefined
2484
+ : async (recipeName) => {
2485
+ const { findAllLiveBridges } = await import("./bridgeLockDiscovery.js");
2486
+ const liveLocks = findAllLiveBridges();
2487
+ if (liveLocks.length === 0)
2488
+ return null;
2489
+ const sinceMs = Date.now() - SEVEN_DAYS_MS;
2490
+ const qs = `?sinceMs=${sinceMs}&recipe=${encodeURIComponent(recipeName)}`;
2491
+ for (const lock of liveLocks) {
2492
+ const controller = new AbortController();
2493
+ const timer = setTimeout(() => controller.abort(), 10_000);
2494
+ try {
2495
+ const res = await fetch(`http://127.0.0.1:${lock.port}/runs/halt-summary${qs}`, {
2496
+ headers: { Authorization: `Bearer ${lock.authToken}` },
2497
+ signal: controller.signal,
2498
+ });
2499
+ if (res.ok) {
2500
+ return (await res.json());
2501
+ }
2502
+ }
2503
+ catch {
2504
+ /* unreachable lock — try next */
2505
+ }
2506
+ finally {
2507
+ clearTimeout(timer);
2508
+ }
2509
+ }
2510
+ return null;
2511
+ };
2512
+ // Connector auth fetcher: walk live bridges, GET /connections to read
2513
+ // each connector's status. Returns null when no bridge is reachable so
2514
+ // doctor degrades to static-only (lists required connectors, no auth).
2515
+ const fetchConnections = localOnly
2516
+ ? undefined
2517
+ : async () => {
2518
+ const { findAllLiveBridges } = await import("./bridgeLockDiscovery.js");
2519
+ const liveLocks = findAllLiveBridges();
2520
+ if (liveLocks.length === 0)
2521
+ return null;
2522
+ for (const lock of liveLocks) {
2523
+ const controller = new AbortController();
2524
+ const timer = setTimeout(() => controller.abort(), 10_000);
2525
+ try {
2526
+ const res = await fetch(`http://127.0.0.1:${lock.port}/connections`, {
2527
+ headers: { Authorization: `Bearer ${lock.authToken}` },
2528
+ signal: controller.signal,
2529
+ });
2530
+ if (res.ok) {
2531
+ const body = (await res.json());
2532
+ return body.connectors ?? [];
2533
+ }
2534
+ }
2535
+ catch {
2536
+ /* unreachable lock — try next */
2537
+ }
2538
+ finally {
2539
+ clearTimeout(timer);
2540
+ }
2541
+ }
2542
+ return null;
2543
+ };
2544
+ const result = await runRecipeDoctor(ref, {
2545
+ fetchHalts,
2546
+ fetchConnections,
2547
+ });
2548
+ if (wantJson) {
2549
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
2550
+ }
2551
+ else {
2552
+ process.stdout.write(formatRecipeDoctorReport(result));
2553
+ }
2554
+ process.exit(result.ok ? 0 : 1);
2555
+ }
2556
+ catch (err) {
2557
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
2558
+ process.exit(1);
2559
+ }
2560
+ })();
2561
+ }
1637
2562
  // Patchwork: `patchwork recipe fmt <file.yaml>` — format/normalize recipe.
1638
2563
  if (process.argv[2] === "recipe" && process.argv[3] === "fmt") {
1639
2564
  const args = process.argv.slice(4);
@@ -1952,9 +2877,12 @@ Steps performed:
1952
2877
  }
1953
2878
  if (extensionArg2) {
1954
2879
  try {
2880
+ // Windows: editor binaries (code/cursor/windsurf) are `.cmd` shims that
2881
+ // Node's execFileSync can't launch without a shell. See bridgeProcess.ts.
1955
2882
  execFileSync(editor, ["--install-extension", extensionArg2], {
1956
2883
  stdio: "pipe",
1957
2884
  timeout: 30000,
2885
+ shell: process.platform === "win32",
1958
2886
  });
1959
2887
  process.stderr.write(` ✓ Extension installed via ${editor}\n\n`);
1960
2888
  }
@@ -2083,13 +3011,18 @@ Steps performed:
2083
3011
  process.stderr.write(` ✓ MCP shim — already registered in ${claudeJsonAbs}\n\n`);
2084
3012
  }
2085
3013
  else {
3014
+ // claude -p spawns the stdio command via Node's child_process, which
3015
+ // can't resolve a bare `.cmd` shim on Windows. Record the `.cmd` form
3016
+ // on win32 so the bridge binary is findable by the spawned process.
2086
3017
  mcpServers["claude-ide-bridge"] = {
2087
- command: "claude-ide-bridge",
3018
+ command: ensureCmdShim("claude-ide-bridge"),
2088
3019
  args: ["shim"],
2089
3020
  type: "stdio",
2090
3021
  };
2091
3022
  claudeJson.mcpServers = mcpServers;
2092
- writeFileSync(claudeJsonAbs, `${JSON.stringify(claudeJson, null, 2)}\n`);
3023
+ // Atomic — `~/.claude.json` holds every MCP server registration on
3024
+ // the machine. A crash mid-write would brick Claude Code globally.
3025
+ writeFileAtomicSync(claudeJsonAbs, `${JSON.stringify(claudeJson, null, 2)}\n`);
2093
3026
  process.stderr.write(` ✓ MCP shim — registered in ${claudeJsonAbs}\n Note: bridge tools are wired via ~/.claude.json (global), not .mcp.json.\n This is intentional — when VS Code/Windsurf/Cursor launches Claude Code it\n injects --mcp-config which overrides any project .mcp.json. Only ~/.claude.json\n is always loaded. You do not need to add anything to .mcp.json.\n\n`);
2094
3027
  }
2095
3028
  }
@@ -2140,7 +3073,9 @@ Steps performed:
2140
3073
  }
2141
3074
  if (added.length > 0 || migrated.length > 0) {
2142
3075
  ccSettings.hooks = ccHooks;
2143
- writeFileSync(ccSettingsPath, `${JSON.stringify(ccSettings, null, 2)}\n`);
3076
+ // Atomic — `~/.claude/settings.json` holds every CC hook entry; a
3077
+ // crash mid-write loses the user's full hook configuration.
3078
+ writeFileAtomicSync(ccSettingsPath, `${JSON.stringify(ccSettings, null, 2)}\n`);
2144
3079
  const addMsg = added.length > 0
2145
3080
  ? ` ✓ CC hooks — wired ${added.length} automation hook(s) in ${ccSettingsPath}\n Added: ${added.join(", ")}\n`
2146
3081
  : "";
@@ -2181,6 +3116,9 @@ Steps performed:
2181
3116
  execFileSync("claude-ide-bridge", ["--version"], {
2182
3117
  stdio: "pipe",
2183
3118
  timeout: 5000,
3119
+ // Windows: global npm bin is a `.cmd` shim that Node's execFileSync
3120
+ // can't launch without a shell. See bridgeProcess.ts for context.
3121
+ shell: process.platform === "win32",
2184
3122
  });
2185
3123
  shimOnPath = true;
2186
3124
  }
@@ -2240,13 +3178,8 @@ Steps performed:
2240
3178
  let hooksWired = false;
2241
3179
  try {
2242
3180
  const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
2243
- const sj = JSON.parse(readFileSync(settingsPath, "utf-8"));
2244
- const hooksObj = sj?.hooks;
2245
- if (hooksObj && typeof hooksObj === "object") {
2246
- hooksWired = Object.values(hooksObj).flat().some((e) => typeof e?.command ===
2247
- "string" &&
2248
- (e.command ?? "").includes("claude-ide-bridge"));
2249
- }
3181
+ const { isPreToolUseHookRegistered } = await import("./preToolUseHook.js");
3182
+ hooksWired = isPreToolUseHookRegistered(settingsPath);
2250
3183
  }
2251
3184
  catch {
2252
3185
  /* file may not exist yet — non-fatal */
@@ -2264,8 +3197,11 @@ Steps performed:
2264
3197
  ? fallbackDocs
2265
3198
  : null;
2266
3199
  if (target) {
2267
- const { exec } = await import("node:child_process");
2268
- exec(`code "${target}"`, { timeout: 3000 }, () => { });
3200
+ // Use execFile with argv (no shell) — exec(`code "${target}"`) was
3201
+ // shell-evaluated and could be injected via `--workspace '"; ...'`
3202
+ // since path.resolve preserves shell metachars. Audit 2026-05-17.
3203
+ const { execFile } = await import("node:child_process");
3204
+ execFile("code", [target], { timeout: 3000 }, () => { });
2269
3205
  }
2270
3206
  }
2271
3207
  // Analytics opt-in prompt — only ask once; skip if preference already set
@@ -2382,9 +3318,11 @@ else if (process.argv[2] === "install-extension") {
2382
3318
  }
2383
3319
  try {
2384
3320
  process.stderr.write(`Installing extension via ${editor}...\n`);
3321
+ // Windows: editor binaries are `.cmd` shims; need shell for resolution.
2385
3322
  execFileSync(editor, ["--install-extension", extensionArg], {
2386
3323
  stdio: "inherit",
2387
3324
  timeout: 30000,
3325
+ shell: process.platform === "win32",
2388
3326
  });
2389
3327
  process.stderr.write("Extension installed successfully.\n");
2390
3328
  }
@@ -2561,6 +3499,88 @@ Options:
2561
3499
  }
2562
3500
  process.exit(0);
2563
3501
  }
3502
+ // `patchwork doctor` — run CLI-safe bridge health checks.
3503
+ if (process.argv[2] === "doctor") {
3504
+ const args = process.argv.slice(3);
3505
+ if (args.includes("--help") || args.includes("-h")) {
3506
+ process.stdout.write("Usage: patchwork doctor [--workspace <path>] [--port <n>] [--json]\n\n" +
3507
+ "Runs bridge health checks (workspace, git, lock file, automation policy).\n" +
3508
+ "Exits 1 if any check fails.\n");
3509
+ process.exit(0);
3510
+ }
3511
+ (async () => {
3512
+ try {
3513
+ const workspaceIdx = args.indexOf("--workspace");
3514
+ const workspace = workspaceIdx !== -1 && args[workspaceIdx + 1]
3515
+ ? args[workspaceIdx + 1]
3516
+ : process.cwd();
3517
+ const portIdx = args.indexOf("--port");
3518
+ const portArg = portIdx !== -1 ? args[portIdx + 1] : undefined;
3519
+ const port = portArg !== undefined ? Number(portArg) : undefined;
3520
+ const jsonFlag = args.includes("--json");
3521
+ const { runDoctor } = await import("./commands/doctor.js");
3522
+ const result = await runDoctor({ workspace, port, json: jsonFlag });
3523
+ if (jsonFlag) {
3524
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
3525
+ }
3526
+ else {
3527
+ for (const check of result.checks) {
3528
+ const icon = check.status === "ok" ? "✓" : check.status === "warn" ? "⚠" : "✗";
3529
+ const detail = check.detail ? ` ${check.detail}` : "";
3530
+ process.stdout.write(` ${icon} ${check.name}${detail}\n`);
3531
+ if (check.suggestion) {
3532
+ process.stdout.write(` → ${check.suggestion}\n`);
3533
+ }
3534
+ }
3535
+ }
3536
+ process.exit(result.ok ? 0 : 1);
3537
+ }
3538
+ catch (err) {
3539
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
3540
+ process.exit(1);
3541
+ }
3542
+ })();
3543
+ }
3544
+ // `patchwork shadow-scan` — replay run history through the destructive-tool classifier.
3545
+ if (process.argv[2] === "shadow-scan") {
3546
+ const args = process.argv.slice(3);
3547
+ if (args.includes("--help") || args.includes("-h")) {
3548
+ process.stdout.write("Usage: patchwork shadow-scan [--since <duration|ISO>] [--limit <n>] [--runs-file <path>] [--json]\n\n" +
3549
+ "Replays historical run data through the destructive-tool classifier.\n" +
3550
+ "Exits 1 if any runs would be reclassified.\n\n" +
3551
+ " --since <duration|ISO> Lookback window, e.g. '24h', '7d', or ISO date (default: 7d)\n" +
3552
+ " --limit <n> Cap the number of runs to scan\n" +
3553
+ " --runs-file <path> Override default ~/.claude/ide/runs.jsonl path\n" +
3554
+ " --json Emit JSON output\n");
3555
+ process.exit(0);
3556
+ }
3557
+ (async () => {
3558
+ try {
3559
+ const sinceIdx = args.indexOf("--since");
3560
+ const since = sinceIdx !== -1 ? args[sinceIdx + 1] : undefined;
3561
+ const limitIdx = args.indexOf("--limit");
3562
+ const limitArg = limitIdx !== -1 ? args[limitIdx + 1] : undefined;
3563
+ const limit = limitArg !== undefined ? Number(limitArg) : undefined;
3564
+ const runsFileIdx = args.indexOf("--runs-file");
3565
+ const runsFile = runsFileIdx !== -1 ? args[runsFileIdx + 1] : undefined;
3566
+ const jsonFlag = args.includes("--json");
3567
+ const { runShadowScanCli } = await import("./commands/shadowScan.js");
3568
+ await runShadowScanCli({
3569
+ ...(since !== undefined ? { since } : {}),
3570
+ ...(limit !== undefined ? { limit } : {}),
3571
+ ...(runsFile !== undefined
3572
+ ? { runsFile, workspace: process.cwd() }
3573
+ : {}),
3574
+ json: jsonFlag,
3575
+ });
3576
+ // runShadowScanCli sets process.exitCode = 1 on reclassified > 0; no explicit exit needed.
3577
+ }
3578
+ catch (err) {
3579
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
3580
+ process.exit(1);
3581
+ }
3582
+ })();
3583
+ }
2564
3584
  // Handle launchd subcommand — install/uninstall macOS LaunchAgent for auto-start
2565
3585
  if (process.argv[2] === "launchd") {
2566
3586
  const sub = process.argv[3];
@@ -2581,11 +3601,24 @@ if (process.argv[2] === "launchd") {
2581
3601
  // F6: "Did you mean?" for unknown CLI subcommands
2582
3602
  // Patchwork: no-args → terminal dashboard (when invoked as patchwork-os or patchwork).
2583
3603
  {
2584
- const binName = path.basename(process.argv[1] ?? "");
3604
+ const binName = invokedBinaryName();
2585
3605
  const isPatchworkBin = binName === "patchwork-os" ||
2586
3606
  binName === "patchwork" ||
2587
3607
  binName === "patchwork.js";
2588
3608
  if (isPatchworkBin && (!process.argv[2] || process.argv[2] === "dashboard")) {
3609
+ // First-run guard: if the user hasn't run `patchwork init` yet, launching
3610
+ // the dashboard renders an empty panel with no signpost. Print an
3611
+ // actionable pointer instead and exit cleanly.
3612
+ const cfgPath = path.join(os.homedir(), ".patchwork", "config.json");
3613
+ if (!existsSync(cfgPath) && !process.argv[2]) {
3614
+ process.stdout.write(`No Patchwork config found at ${cfgPath}.\n\n` +
3615
+ `Run \`${binName} init\` to scaffold ~/.patchwork and wire up\n` +
3616
+ `Claude Code hooks, then \`${binName}\` again to open the dashboard.\n\n` +
3617
+ `For just the IDE bridge (no recipes / approval queue), run:\n` +
3618
+ ` ${binName} install-extension\n` +
3619
+ ` ${binName} --workspace .\n`);
3620
+ process.exit(0);
3621
+ }
2589
3622
  (async () => {
2590
3623
  const { runDashboard } = await import("./commands/dashboard.js");
2591
3624
  await runDashboard();
@@ -2663,8 +3696,10 @@ else {
2663
3696
  .digest("hex")
2664
3697
  .slice(0, 6);
2665
3698
  const sessionName = `claude-bridge-${ws}${hash}`;
2666
- // Check if tmux is available
2667
- const tmuxCheck = spawnSync("which", ["tmux"], { stdio: "ignore" });
3699
+ // Check if tmux is available (skip on Windows — tmux doesn't exist there)
3700
+ const tmuxCheck = process.platform !== "win32"
3701
+ ? spawnSync("which", ["tmux"], { stdio: "ignore" })
3702
+ : { status: 1 };
2668
3703
  if (tmuxCheck.status !== 0) {
2669
3704
  process.stderr.write("WARNING: --auto-tmux requested but tmux is not installed. Running without tmux.\n");
2670
3705
  }
@@ -2713,7 +3748,12 @@ else {
2713
3748
  for (const sig of ["SIGTERM", "SIGINT"]) {
2714
3749
  process.once(sig, () => {
2715
3750
  stopping = true;
2716
- child.kill(sig);
3751
+ // Use treeKill so grandchildren (recipe runners, claude
3752
+ // subprocesses, extension watchers) are reaped on Windows.
3753
+ // Bare `child.kill(sig)` maps to TerminateProcess on win32
3754
+ // and skips descendants → orphaned processes survive a
3755
+ // supervisor SIGTERM. Audit 2026-05-17.
3756
+ treeKill(child, sig);
2717
3757
  });
2718
3758
  }
2719
3759
  child.on("exit", (code, signal) => {
@@ -2741,19 +3781,27 @@ else {
2741
3781
  process.stderr.write(`Error: ${message}\n`);
2742
3782
  process.exit(1);
2743
3783
  });
2744
- // F5: Silent self-update nudge (fire-and-forget)
2745
- import("node:child_process")
2746
- .then(({ exec }) => {
2747
- exec("npm view claude-ide-bridge version", { timeout: 5000 }, (err, stdout) => {
2748
- if (err || !stdout)
2749
- return;
2750
- const latest = stdout.trim();
2751
- if (latest && semverGt(latest, PACKAGE_VERSION)) {
2752
- console.log(`\n Bridge v${latest} available — run: npm update -g claude-ide-bridge\n`);
2753
- }
2754
- });
2755
- })
2756
- .catch(() => { });
3784
+ // F5: Silent self-update nudge (fire-and-forget).
3785
+ // Skip when running from a source tree (any of: a `.git` sibling of the
3786
+ // package, or __dirnameTop not under a node_modules/). Otherwise a dev
3787
+ // who built locally sees "Bridge v<X> available" pointing at an npm
3788
+ // install path they're not using.
3789
+ const isSourceBuild = existsSync(path.join(__dirnameTop, "..", ".git")) ||
3790
+ !__dirnameTop.includes(`${path.sep}node_modules${path.sep}`);
3791
+ if (!isSourceBuild) {
3792
+ import("node:child_process")
3793
+ .then(({ exec }) => {
3794
+ exec("npm view patchwork-os version", { timeout: 5000 }, (err, stdout) => {
3795
+ if (err || !stdout)
3796
+ return;
3797
+ const latest = stdout.trim();
3798
+ if (latest && semverGt(latest, PACKAGE_VERSION)) {
3799
+ console.log(`\n Patchwork OS v${latest} available — run: npm update -g patchwork-os\n`);
3800
+ }
3801
+ });
3802
+ })
3803
+ .catch(() => { });
3804
+ }
2757
3805
  }
2758
3806
  } // end of `else` for `if (__subcommandWillRun)` (bridge-mode block)
2759
3807
  //# sourceMappingURL=index.js.map