patchwork-os 0.2.0-beta.2 → 0.2.0-beta.4

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 (261) hide show
  1. package/README.bridge.md +5 -5
  2. package/README.md +244 -30
  3. package/dist/activityLog.d.ts +6 -0
  4. package/dist/activityLog.js +10 -1
  5. package/dist/activityLog.js.map +1 -1
  6. package/dist/analyticsPrefs.d.ts +35 -2
  7. package/dist/analyticsPrefs.js +120 -21
  8. package/dist/analyticsPrefs.js.map +1 -1
  9. package/dist/analyticsSend.js +5 -1
  10. package/dist/analyticsSend.js.map +1 -1
  11. package/dist/approvalHttp.js +25 -8
  12. package/dist/approvalHttp.js.map +1 -1
  13. package/dist/approvalQueue.d.ts +44 -1
  14. package/dist/approvalQueue.js +117 -0
  15. package/dist/approvalQueue.js.map +1 -1
  16. package/dist/automation.d.ts +3 -3
  17. package/dist/automation.js +12 -5
  18. package/dist/automation.js.map +1 -1
  19. package/dist/bridge.d.ts +2 -0
  20. package/dist/bridge.js +140 -8
  21. package/dist/bridge.js.map +1 -1
  22. package/dist/bridgeLockDiscovery.d.ts +27 -1
  23. package/dist/bridgeLockDiscovery.js +38 -11
  24. package/dist/bridgeLockDiscovery.js.map +1 -1
  25. package/dist/claudeOrchestrator.js +27 -10
  26. package/dist/claudeOrchestrator.js.map +1 -1
  27. package/dist/commands/dashboard.js +8 -1
  28. package/dist/commands/dashboard.js.map +1 -1
  29. package/dist/commands/install.js +3 -0
  30. package/dist/commands/install.js.map +1 -1
  31. package/dist/commands/patchworkInit.d.ts +5 -0
  32. package/dist/commands/patchworkInit.js +89 -7
  33. package/dist/commands/patchworkInit.js.map +1 -1
  34. package/dist/commands/recipe.d.ts +51 -0
  35. package/dist/commands/recipe.js +353 -2
  36. package/dist/commands/recipe.js.map +1 -1
  37. package/dist/commands/recipeInstall.js +6 -3
  38. package/dist/commands/recipeInstall.js.map +1 -1
  39. package/dist/commands/task.js +2 -2
  40. package/dist/commands/task.js.map +1 -1
  41. package/dist/commitIssueLinkLog.d.ts +16 -0
  42. package/dist/commitIssueLinkLog.js +87 -4
  43. package/dist/commitIssueLinkLog.js.map +1 -1
  44. package/dist/config.d.ts +29 -3
  45. package/dist/config.js +77 -21
  46. package/dist/config.js.map +1 -1
  47. package/dist/connectorRoutes.js +1 -1
  48. package/dist/connectorRoutes.js.map +1 -1
  49. package/dist/connectors/asana.js +4 -3
  50. package/dist/connectors/asana.js.map +1 -1
  51. package/dist/connectors/confluence.js +35 -0
  52. package/dist/connectors/confluence.js.map +1 -1
  53. package/dist/connectors/datadog.js +33 -4
  54. package/dist/connectors/datadog.js.map +1 -1
  55. package/dist/connectors/discord.js +5 -4
  56. package/dist/connectors/discord.js.map +1 -1
  57. package/dist/connectors/gitlab.js +7 -1
  58. package/dist/connectors/gitlab.js.map +1 -1
  59. package/dist/connectors/mcpOAuth.js +71 -6
  60. package/dist/connectors/mcpOAuth.js.map +1 -1
  61. package/dist/connectors/slack.d.ts +1 -1
  62. package/dist/connectors/slack.js +56 -4
  63. package/dist/connectors/slack.js.map +1 -1
  64. package/dist/connectors/tokenStorage.js +56 -14
  65. package/dist/connectors/tokenStorage.js.map +1 -1
  66. package/dist/decisionTraceLog.d.ts +28 -0
  67. package/dist/decisionTraceLog.js +115 -7
  68. package/dist/decisionTraceLog.js.map +1 -1
  69. package/dist/drivers/claude/subprocess.js +22 -3
  70. package/dist/drivers/claude/subprocess.js.map +1 -1
  71. package/dist/drivers/gemini/index.js +19 -3
  72. package/dist/drivers/gemini/index.js.map +1 -1
  73. package/dist/extensionClient.d.ts +29 -4
  74. package/dist/extensionClient.js +26 -11
  75. package/dist/extensionClient.js.map +1 -1
  76. package/dist/featureFlags.d.ts +76 -0
  77. package/dist/featureFlags.js +153 -3
  78. package/dist/featureFlags.js.map +1 -1
  79. package/dist/fileLockSync.d.ts +67 -0
  80. package/dist/fileLockSync.js +126 -0
  81. package/dist/fileLockSync.js.map +1 -0
  82. package/dist/fp/automationInterpreter.d.ts +6 -0
  83. package/dist/fp/automationInterpreter.js +15 -2
  84. package/dist/fp/automationInterpreter.js.map +1 -1
  85. package/dist/fp/automationState.d.ts +1 -1
  86. package/dist/fp/automationState.js +10 -0
  87. package/dist/fp/automationState.js.map +1 -1
  88. package/dist/fp/commandDescription.js +7 -1
  89. package/dist/fp/commandDescription.js.map +1 -1
  90. package/dist/fsWatchWithFallback.d.ts +36 -0
  91. package/dist/fsWatchWithFallback.js +127 -0
  92. package/dist/fsWatchWithFallback.js.map +1 -0
  93. package/dist/index.js +797 -75
  94. package/dist/index.js.map +1 -1
  95. package/dist/installGuard.js +6 -2
  96. package/dist/installGuard.js.map +1 -1
  97. package/dist/lockfile.js +31 -4
  98. package/dist/lockfile.js.map +1 -1
  99. package/dist/patchworkConfig.js +13 -3
  100. package/dist/patchworkConfig.js.map +1 -1
  101. package/dist/pluginLoader.js +10 -1
  102. package/dist/pluginLoader.js.map +1 -1
  103. package/dist/pluginWatcher.js +6 -13
  104. package/dist/pluginWatcher.js.map +1 -1
  105. package/dist/preToolUseHook.js +3 -2
  106. package/dist/preToolUseHook.js.map +1 -1
  107. package/dist/processTree.d.ts +34 -0
  108. package/dist/processTree.js +105 -0
  109. package/dist/processTree.js.map +1 -0
  110. package/dist/prompts.js +3 -3
  111. package/dist/prompts.js.map +1 -1
  112. package/dist/recipeOrchestration.js +35 -1
  113. package/dist/recipeOrchestration.js.map +1 -1
  114. package/dist/recipeRoutes.d.ts +37 -0
  115. package/dist/recipeRoutes.js +236 -33
  116. package/dist/recipeRoutes.js.map +1 -1
  117. package/dist/recipes/agentExecutor.d.ts +25 -5
  118. package/dist/recipes/agentExecutor.js.map +1 -1
  119. package/dist/recipes/chainedRunner.js +16 -2
  120. package/dist/recipes/chainedRunner.js.map +1 -1
  121. package/dist/recipes/connectorPreflight.d.ts +53 -0
  122. package/dist/recipes/connectorPreflight.js +143 -0
  123. package/dist/recipes/connectorPreflight.js.map +1 -0
  124. package/dist/recipes/githubInstallSource.d.ts +62 -0
  125. package/dist/recipes/githubInstallSource.js +125 -0
  126. package/dist/recipes/githubInstallSource.js.map +1 -0
  127. package/dist/recipes/haltCategory.d.ts +80 -0
  128. package/dist/recipes/haltCategory.js +125 -0
  129. package/dist/recipes/haltCategory.js.map +1 -0
  130. package/dist/recipes/idempotencyKey.d.ts +126 -0
  131. package/dist/recipes/idempotencyKey.js +297 -0
  132. package/dist/recipes/idempotencyKey.js.map +1 -0
  133. package/dist/recipes/installer.js +48 -2
  134. package/dist/recipes/installer.js.map +1 -1
  135. package/dist/recipes/judgeSummary.d.ts +50 -0
  136. package/dist/recipes/judgeSummary.js +47 -0
  137. package/dist/recipes/judgeSummary.js.map +1 -0
  138. package/dist/recipes/judgeVerdict.d.ts +48 -0
  139. package/dist/recipes/judgeVerdict.js +174 -0
  140. package/dist/recipes/judgeVerdict.js.map +1 -0
  141. package/dist/recipes/migrations/index.d.ts +9 -0
  142. package/dist/recipes/migrations/index.js +133 -0
  143. package/dist/recipes/migrations/index.js.map +1 -1
  144. package/dist/recipes/parser.js +82 -4
  145. package/dist/recipes/parser.js.map +1 -1
  146. package/dist/recipes/runBudget.d.ts +70 -0
  147. package/dist/recipes/runBudget.js +109 -0
  148. package/dist/recipes/runBudget.js.map +1 -0
  149. package/dist/recipes/scheduler.d.ts +17 -0
  150. package/dist/recipes/scheduler.js +34 -2
  151. package/dist/recipes/scheduler.js.map +1 -1
  152. package/dist/recipes/schema.d.ts +30 -0
  153. package/dist/recipes/toolRegistry.js +19 -0
  154. package/dist/recipes/toolRegistry.js.map +1 -1
  155. package/dist/recipes/tools/http.d.ts +10 -0
  156. package/dist/recipes/tools/http.js +176 -0
  157. package/dist/recipes/tools/http.js.map +1 -0
  158. package/dist/recipes/tools/index.d.ts +1 -0
  159. package/dist/recipes/tools/index.js +1 -0
  160. package/dist/recipes/tools/index.js.map +1 -1
  161. package/dist/recipes/validation.js +1 -1
  162. package/dist/recipes/validation.js.map +1 -1
  163. package/dist/recipes/yamlRunner.d.ts +75 -8
  164. package/dist/recipes/yamlRunner.js +174 -28
  165. package/dist/recipes/yamlRunner.js.map +1 -1
  166. package/dist/resources.js +21 -13
  167. package/dist/resources.js.map +1 -1
  168. package/dist/runLog.d.ts +28 -0
  169. package/dist/runLog.js +19 -3
  170. package/dist/runLog.js.map +1 -1
  171. package/dist/sanitizeParsedJson.d.ts +39 -0
  172. package/dist/sanitizeParsedJson.js +55 -0
  173. package/dist/sanitizeParsedJson.js.map +1 -0
  174. package/dist/server.d.ts +79 -0
  175. package/dist/server.js +356 -3
  176. package/dist/server.js.map +1 -1
  177. package/dist/sessionCheckpoint.d.ts +8 -0
  178. package/dist/sessionCheckpoint.js +18 -2
  179. package/dist/sessionCheckpoint.js.map +1 -1
  180. package/dist/streamableHttp.js +17 -6
  181. package/dist/streamableHttp.js.map +1 -1
  182. package/dist/tools/bridgeDoctor.js +6 -2
  183. package/dist/tools/bridgeDoctor.js.map +1 -1
  184. package/dist/tools/detectUnusedCode.js +9 -7
  185. package/dist/tools/detectUnusedCode.js.map +1 -1
  186. package/dist/tools/editText.js +2 -1
  187. package/dist/tools/editText.js.map +1 -1
  188. package/dist/tools/fileOperations.js +2 -1
  189. package/dist/tools/fileOperations.js.map +1 -1
  190. package/dist/tools/fileWatcher.js +8 -2
  191. package/dist/tools/fileWatcher.js.map +1 -1
  192. package/dist/tools/fixAllLintErrors.js +10 -5
  193. package/dist/tools/fixAllLintErrors.js.map +1 -1
  194. package/dist/tools/formatDocument.js +10 -5
  195. package/dist/tools/formatDocument.js.map +1 -1
  196. package/dist/tools/getCodeCoverage.js +7 -3
  197. package/dist/tools/getCodeCoverage.js.map +1 -1
  198. package/dist/tools/handoffNote.js +2 -1
  199. package/dist/tools/handoffNote.js.map +1 -1
  200. package/dist/tools/headless/lspClient.js +3 -0
  201. package/dist/tools/headless/lspClient.js.map +1 -1
  202. package/dist/tools/lsp.js +17 -0
  203. package/dist/tools/lsp.js.map +1 -1
  204. package/dist/tools/openDiff.js +4 -1
  205. package/dist/tools/openDiff.js.map +1 -1
  206. package/dist/tools/openFile.js +4 -1
  207. package/dist/tools/openFile.js.map +1 -1
  208. package/dist/tools/organizeImports.js +5 -3
  209. package/dist/tools/organizeImports.js.map +1 -1
  210. package/dist/tools/previewEdit.js +7 -2
  211. package/dist/tools/previewEdit.js.map +1 -1
  212. package/dist/tools/recentTracesDigest.js +56 -11
  213. package/dist/tools/recentTracesDigest.js.map +1 -1
  214. package/dist/tools/refactorExtractFunction.js +4 -1
  215. package/dist/tools/refactorExtractFunction.js.map +1 -1
  216. package/dist/tools/refactorPreview.js +10 -2
  217. package/dist/tools/refactorPreview.js.map +1 -1
  218. package/dist/tools/replaceBlock.js +2 -1
  219. package/dist/tools/replaceBlock.js.map +1 -1
  220. package/dist/tools/searchAndReplace.js +2 -1
  221. package/dist/tools/searchAndReplace.js.map +1 -1
  222. package/dist/tools/spawnWorkspace.js +15 -7
  223. package/dist/tools/spawnWorkspace.js.map +1 -1
  224. package/dist/tools/testRunners/vitestJest.js +3 -1
  225. package/dist/tools/testRunners/vitestJest.js.map +1 -1
  226. package/dist/tools/transaction.js +4 -1
  227. package/dist/tools/transaction.js.map +1 -1
  228. package/dist/tools/utils.js +68 -8
  229. package/dist/tools/utils.js.map +1 -1
  230. package/dist/transport.d.ts +1 -1
  231. package/dist/transport.js +18 -4
  232. package/dist/transport.js.map +1 -1
  233. package/dist/winShim.d.ts +34 -0
  234. package/dist/winShim.js +94 -0
  235. package/dist/winShim.js.map +1 -0
  236. package/dist/writeFileAtomic.d.ts +23 -0
  237. package/dist/writeFileAtomic.js +94 -0
  238. package/dist/writeFileAtomic.js.map +1 -0
  239. package/package.json +17 -6
  240. package/scripts/postinstall.mjs +42 -2
  241. package/scripts/smoke/run-all.mjs +213 -0
  242. package/scripts/start-all.mjs +572 -0
  243. package/scripts/start-all.ps1 +209 -0
  244. package/scripts/start-all.sh +73 -17
  245. package/scripts/start-orchestrator.ps1 +158 -0
  246. package/scripts/start-remote.mjs +122 -0
  247. package/templates/automation-policies/recipe-authoring.json +1 -1
  248. package/templates/automation-policies/security-first.json +1 -1
  249. package/templates/automation-policies/strict-lint.json +1 -1
  250. package/templates/automation-policies/test-driven.json +1 -1
  251. package/templates/automation-policy.example.json +1 -1
  252. package/templates/co.patchwork-os.bridge.plist +1 -1
  253. package/templates/recipes/approval-queue-ui-test.yaml +1 -1
  254. package/templates/recipes/ctx-loop-test.yaml +1 -1
  255. package/templates/recipes/webhook/apple-watch-health-log.yaml +145 -0
  256. package/dist/commands/marketplace.d.ts +0 -16
  257. package/dist/commands/marketplace.js +0 -32
  258. package/dist/commands/marketplace.js.map +0 -1
  259. package/dist/recipes/legacyRecipeCompat.d.ts +0 -10
  260. package/dist/recipes/legacyRecipeCompat.js +0 -131
  261. package/dist/recipes/legacyRecipeCompat.js.map +0 -1
package/dist/index.js CHANGED
@@ -41,7 +41,10 @@ import { Bridge } from "./bridge.js";
41
41
  import { isBridgeToolsFileValid, repairBridgeToolsRulesIfStale, } from "./bridgeToolsRules.js";
42
42
  import { findEditor, parseConfig } from "./config.js";
43
43
  import { detectWorkspaceSymlinkInstall, PATCHWORK_PACKAGE_NAME, SYMLINK_INSTALL_FIX, } from "./installGuard.js";
44
+ import { treeKill } from "./processTree.js";
44
45
  import { PACKAGE_VERSION, semverGt } from "./version.js";
46
+ import { ensureCmdShim } from "./winShim.js";
47
+ import { writeFileAtomicSync } from "./writeFileAtomic.js";
45
48
  const __dirnameTop = path.dirname(fileURLToPath(import.meta.url));
46
49
  // Warn when a symlinked global install is detected (`npm install -g .`).
47
50
  // launchctl / sandbox environments can fail through that link with EPERM.
@@ -119,7 +122,6 @@ const KNOWN_SUBCOMMANDS = [
119
122
  "gen-plugin-stub",
120
123
  "notify",
121
124
  "install",
122
- "marketplace",
123
125
  "status",
124
126
  "shim",
125
127
  "recipe",
@@ -128,6 +130,10 @@ const KNOWN_SUBCOMMANDS = [
128
130
  "dashboard",
129
131
  "launchd",
130
132
  "start",
133
+ "kill-switch",
134
+ "panic",
135
+ "halts",
136
+ "judgments",
131
137
  ];
132
138
  const __invokedSubcommand = (() => {
133
139
  const sub = process.argv[2];
@@ -139,10 +145,20 @@ const __invokedSubcommand = (() => {
139
145
  ? sub
140
146
  : null;
141
147
  })();
148
+ // bash/zsh set process.env._ to the actual invoked binary path (e.g. /usr/local/bin/patchwork-os).
149
+ // More reliable than argv[1] which resolves to the .js entrypoint via npm global shim.
150
+ function invokedBinaryName() {
151
+ const fromEnv = process.env._
152
+ ? path.basename(process.env._).replace(/\.(cmd|js)$/i, "")
153
+ : "";
154
+ if (fromEnv && fromEnv !== "node" && fromEnv !== "npm")
155
+ return fromEnv;
156
+ return path.basename(process.argv[1] ?? "").replace(/\.js$/, "");
157
+ }
142
158
  const __invokedBareBinaryDashboard = (() => {
143
159
  if (process.argv[2] && process.argv[2] !== "dashboard")
144
160
  return false;
145
- const binName = path.basename(process.argv[1] ?? "");
161
+ const binName = invokedBinaryName();
146
162
  return (binName === "patchwork-os" ||
147
163
  binName === "patchwork" ||
148
164
  binName === "patchwork.js");
@@ -153,6 +169,44 @@ if (process.argv[2] === "--version" || process.argv[2] === "-v") {
153
169
  console.log(`claude-ide-bridge ${PACKAGE_VERSION}`);
154
170
  process.exit(0);
155
171
  }
172
+ // Handle top-level --help / -h / help — print a grouped command index so a
173
+ // first-time user has a discoverable entry point. Without this, bare
174
+ // `patchwork --help` falls through to bridge-daemon arg parsing and errors.
175
+ if (process.argv[2] === "--help" ||
176
+ process.argv[2] === "-h" ||
177
+ process.argv[2] === "help") {
178
+ const binName = path.basename(process.argv[1] ?? "patchwork");
179
+ process.stdout.write(`${binName} ${PACKAGE_VERSION}\n\n` +
180
+ `First time? Run:\n` +
181
+ ` ${binName} init # set up ~/.patchwork + Claude Code hooks\n` +
182
+ ` ${binName} start-all # bridge + Claude + dashboard\n\n` +
183
+ `Get started\n` +
184
+ ` init [--workspace <dir>] Scaffold ~/.patchwork; register CC hooks\n` +
185
+ ` install-extension Install the VS Code / Cursor / Windsurf extension\n` +
186
+ ` start-all [--no-dashboard] Launch bridge + Claude --ide + dashboard\n` +
187
+ ` start-orchestrator Multi-IDE-window meta-bridge\n\n` +
188
+ `Recipes\n` +
189
+ ` recipe new <name> [-i] Scaffold a recipe\n` +
190
+ ` recipe list List installed recipes\n` +
191
+ ` recipe run <name> [--vars k=v] Run a recipe by name\n` +
192
+ ` recipe install <source> Install from a path or GitHub source\n` +
193
+ ` recipe --help Full recipe subcommand index\n\n` +
194
+ `Diagnose\n` +
195
+ ` halts [--window 1h|24h|overnight|7d] Morning summary of recent recipe halts\n` +
196
+ ` traces export Bundle approval / recipe / decision traces\n` +
197
+ ` print-token [--port N] Print the active bridge auth token\n\n` +
198
+ `Daemon (no subcommand)\n` +
199
+ ` --workspace <dir> Start the bridge in foreground\n` +
200
+ ` --watch Auto-restart supervisor\n` +
201
+ ` --slim 27 IDE-only tools (default: full)\n\n` +
202
+ `Other\n` +
203
+ ` --version, -v Print package version\n` +
204
+ ` shim stdio↔WebSocket shim (used by MCP clients)\n` +
205
+ ` notify <event> Notify a running bridge of a CC hook event\n\n` +
206
+ `Bridge-daemon flags: run \`${binName} --workspace . --help-flags\` for the full list,\n` +
207
+ `or see https://github.com/Oolab-labs/patchwork-os#readme.\n`);
208
+ process.exit(0);
209
+ }
156
210
  // Handle patchwork-init subcommand — T2 from docs/install-ux-plan.md.
157
211
  // Separate from the bridge-only `init` to preserve back-compat. See ADR-0008.
158
212
  if (process.argv[2] === "patchwork-init") {
@@ -160,6 +214,13 @@ if (process.argv[2] === "patchwork-init") {
160
214
  await runPatchworkInit(process.argv.slice(3));
161
215
  process.exit(0);
162
216
  }
217
+ // `patchwork-os init` → dashboard setup, not IDE bridge installer.
218
+ // patchwork init / claude-ide-bridge init still go to the bridge path below.
219
+ if (process.argv[2] === "init" && invokedBinaryName() === "patchwork-os") {
220
+ const { runPatchworkInit } = await import("./commands/patchworkInit.js");
221
+ await runPatchworkInit(process.argv.slice(3));
222
+ process.exit(0);
223
+ }
163
224
  // Handle start-all subcommand — launches the full 3-pane tmux orchestrator.
164
225
  // Also triggered when invoked as `claude-ide-bridge-start` directly.
165
226
  const isStartAll = process.argv[2] === "start-all" ||
@@ -168,8 +229,11 @@ if (isStartAll) {
168
229
  const startAllArgs = process.argv[2] === "start-all"
169
230
  ? process.argv.slice(3)
170
231
  : process.argv.slice(2);
171
- const scriptPath = path.resolve(__dirnameTop, "..", "scripts", "start-all.sh");
172
- const result = spawnSync("bash", [scriptPath, ...startAllArgs], {
232
+ // Dispatch the cross-platform Node orchestrator (start-all.mjs). The
233
+ // bash entry-point is kept as a developer shortcut but Windows has no
234
+ // `bash` on PATH by default, and the .mjs is functionally equivalent.
235
+ const scriptPath = path.resolve(__dirnameTop, "..", "scripts", "start-all.mjs");
236
+ const result = spawnSync(process.execPath, [scriptPath, ...startAllArgs], {
173
237
  stdio: "inherit",
174
238
  });
175
239
  process.exit(result.status ?? 1);
@@ -177,14 +241,16 @@ if (isStartAll) {
177
241
  // `patchwork start` — opinionated front door over start-all.
178
242
  // Defaults to full mode (all tools registered) and the web dashboard, so the
179
243
  // doc-promised "patchwork start → everything works" path actually works.
180
- // Pass-through args still go to start-all.sh; --help short-circuits.
244
+ // Pass-through args still go to start-all.mjs; --help short-circuits.
181
245
  if (process.argv[2] === "start") {
182
246
  const passthrough = process.argv.slice(3);
183
247
  if (passthrough.includes("--help") || passthrough.includes("-h")) {
184
248
  process.stdout.write(`patchwork start — Launch the full Patchwork stack
185
249
 
186
- Starts bridge + Claude Code + dashboard in a tmux session.
250
+ Starts bridge + Claude + dashboard via the cross-platform Node orchestrator.
187
251
  Defaults to full mode so all bridge tools are registered.
252
+ On macOS/Linux: uses tmux when available, falls back to background mode.
253
+ On Windows: runs natively via the Node orchestrator (no WSL required).
188
254
 
189
255
  Usage: patchwork start [options]
190
256
 
@@ -206,13 +272,20 @@ This is a thin wrapper over \`start-all\`. For advanced flags see:
206
272
  const args = [...passthrough];
207
273
  const slimIdx = args.indexOf("--slim");
208
274
  if (slimIdx >= 0) {
209
- args.splice(slimIdx, 1); // strip start-all.sh has no --slim flag, slim is its default
275
+ args.splice(slimIdx, 1); // slim is the .mjs default; strip so --full isn't re-added below
210
276
  }
211
277
  else if (!args.includes("--full")) {
212
278
  args.push("--full");
213
279
  }
214
- const scriptPath = path.resolve(__dirnameTop, "..", "scripts", "start-all.sh");
215
- const result = spawnSync("bash", [scriptPath, ...args], {
280
+ // On non-Windows: auto-detect tmux; fall back to --no-tmux background mode if absent.
281
+ if (process.platform !== "win32" && !args.includes("--no-tmux")) {
282
+ const tmuxCheck = spawnSync("which", ["tmux"], { stdio: "ignore" });
283
+ if (tmuxCheck.status !== 0)
284
+ args.push("--no-tmux");
285
+ }
286
+ // Dispatch to the cross-platform Node orchestrator (see above).
287
+ const scriptPath = path.resolve(__dirnameTop, "..", "scripts", "start-all.mjs");
288
+ const result = spawnSync(process.execPath, [scriptPath, ...args], {
216
289
  stdio: "inherit",
217
290
  });
218
291
  process.exit(result.status ?? 1);
@@ -368,13 +441,6 @@ if (process.argv[2] === "install") {
368
441
  await runInstall(process.argv.slice(3));
369
442
  process.exit(0);
370
443
  }
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
444
  // Handle tools subcommand — search/list tools without a bridge connection
379
445
  if (process.argv[2] === "tools") {
380
446
  const { runToolsCommand } = await import("./commands/tools.js");
@@ -885,6 +951,31 @@ Edit, save, hot-reload — Claude's next turn sees the new tool. See [documents/
885
951
  }
886
952
  process.exit(0);
887
953
  }
954
+ // Patchwork: `patchwork recipe` (no subcommand) / `recipe --help` — print
955
+ // the subcommand index. Without this branch, `patchwork recipe` falls through
956
+ // to the bridge daemon, leaving subcommands completely undiscoverable from
957
+ // the CLI (the only way to find them today is to read CLAUDE.md or source).
958
+ if (process.argv[2] === "recipe" &&
959
+ (process.argv[3] === undefined ||
960
+ process.argv[3] === "--help" ||
961
+ process.argv[3] === "-h" ||
962
+ process.argv[3] === "help")) {
963
+ process.stdout.write(`Usage: patchwork recipe <subcommand> [args...]\n\n` +
964
+ `Subcommands:\n` +
965
+ ` new <name> Scaffold a recipe (interactive with -i)\n` +
966
+ ` list List installed recipes (workspace + user)\n` +
967
+ ` run <name> Run a recipe by name\n` +
968
+ ` install <src> Install a recipe from a path or GitHub source\n` +
969
+ ` uninstall <name> Remove an installed recipe\n` +
970
+ ` enable <name> Re-enable a disabled recipe\n` +
971
+ ` disable <name> Pause a recipe (scheduled triggers stop firing)\n` +
972
+ ` preflight <file> Static-validate a recipe YAML before running\n` +
973
+ ` lint <file> Run all lint checks on a recipe YAML\n` +
974
+ ` fmt <file> Format a recipe YAML in place\n` +
975
+ ` schema Print the recipe JSON Schema\n\n` +
976
+ `Run \`patchwork recipe <subcommand> --help\` for subcommand-specific options.\n`);
977
+ process.exit(0);
978
+ }
888
979
  // Patchwork: `patchwork recipe list` — enumerate installed recipes.
889
980
  if (process.argv[2] === "recipe" && process.argv[3] === "list") {
890
981
  (async () => {
@@ -960,11 +1051,13 @@ if (process.argv[2] === "recipe" && process.argv[3] === "uninstall") {
960
1051
  // a running bridge's /recipes/run endpoint if one is available.
961
1052
  if (process.argv[2] === "recipe" && process.argv[3] === "run") {
962
1053
  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";
1054
+ const usage = "Usage: patchwork recipe run <name-or-file> [--local] [--dry-run] [--step <id>] [--var KEY=VALUE] [--attempt <id>] [--ledger-dir <path>]\n";
964
1055
  let localFlag = false;
965
1056
  let dryRun = false;
966
1057
  let recipeRef;
967
1058
  let step;
1059
+ let attemptId;
1060
+ let ledgerDir;
968
1061
  const vars = {};
969
1062
  for (let i = 0; i < args.length; i++) {
970
1063
  const arg = args[i];
@@ -991,6 +1084,29 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
991
1084
  step = value;
992
1085
  continue;
993
1086
  }
1087
+ if (currentArg === "--attempt" || currentArg.startsWith("--attempt=")) {
1088
+ const value = currentArg === "--attempt"
1089
+ ? args[++i]
1090
+ : currentArg.slice("--attempt=".length);
1091
+ if (!value) {
1092
+ process.stderr.write(`Error: --attempt requires a value\n${usage}`);
1093
+ process.exit(1);
1094
+ }
1095
+ attemptId = value;
1096
+ continue;
1097
+ }
1098
+ if (currentArg === "--ledger-dir" ||
1099
+ currentArg.startsWith("--ledger-dir=")) {
1100
+ const value = currentArg === "--ledger-dir"
1101
+ ? args[++i]
1102
+ : currentArg.slice("--ledger-dir=".length);
1103
+ if (!value) {
1104
+ process.stderr.write(`Error: --ledger-dir requires a value\n${usage}`);
1105
+ process.exit(1);
1106
+ }
1107
+ ledgerDir = value;
1108
+ continue;
1109
+ }
994
1110
  if (currentArg === "--var" || currentArg.startsWith("--var=")) {
995
1111
  const assignment = currentArg === "--var" ? args[++i] : currentArg.slice("--var=".length);
996
1112
  if (!assignment) {
@@ -1037,7 +1153,7 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
1037
1153
  })();
1038
1154
  const { findBridgeLock } = await import("./bridgeLockDiscovery.js");
1039
1155
  const lock = localFlag ? null : findBridgeLock();
1040
- if (lock && !dryRun && !step && !explicitFile) {
1156
+ if (lock && !dryRun && !step && !explicitFile && !attemptId) {
1041
1157
  const res = await fetch(`http://127.0.0.1:${lock.port}/recipes/run`, {
1042
1158
  method: "POST",
1043
1159
  headers: {
@@ -1080,9 +1196,37 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
1080
1196
  ? ` Running step "${step}" from recipe "${recipeArg}" locally…\n`
1081
1197
  : ` Running recipe "${recipeArg}" locally…\n`);
1082
1198
  const workdir = lock?.workspace || process.cwd();
1199
+ // PR5c — resume support: when --attempt is given, mint or reuse a
1200
+ // stable id and point the runner at a disk-backed effect ledger.
1201
+ // `--attempt new` always mints a fresh id; any other value is
1202
+ // taken verbatim (so the user can re-run the same attempt and
1203
+ // skip already-completed write tools).
1204
+ let resolvedAttempt;
1205
+ let resolvedLedgerDir;
1206
+ if (attemptId !== undefined) {
1207
+ resolvedAttempt =
1208
+ attemptId === "new"
1209
+ ? `mr_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
1210
+ : attemptId;
1211
+ // Validate at the CLI boundary so an invalid id fails loudly
1212
+ // before any side effects run (and before it lands in the run
1213
+ // log or hashed into a ledger scope key).
1214
+ try {
1215
+ const { assertValidManualRunId } = await import("./recipes/idempotencyKey.js");
1216
+ assertValidManualRunId(resolvedAttempt);
1217
+ }
1218
+ catch (err) {
1219
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1220
+ process.exit(1);
1221
+ }
1222
+ resolvedLedgerDir = ledgerDir ?? path.join(os.homedir(), ".patchwork");
1223
+ process.stdout.write(` Attempt id: ${resolvedAttempt} (ledger: ${resolvedLedgerDir})\n`);
1224
+ }
1083
1225
  const run = await runRecipe(recipeArg, {
1084
1226
  ...(step ? { step } : {}),
1085
1227
  ...(seedVars ? { vars: seedVars } : {}),
1228
+ ...(resolvedAttempt && { manualRunId: resolvedAttempt }),
1229
+ ...(resolvedLedgerDir && { ledgerDir: resolvedLedgerDir }),
1086
1230
  workdir,
1087
1231
  });
1088
1232
  if (run.stepSelection) {
@@ -1435,6 +1579,478 @@ if (process.argv[2] === "traces" && process.argv[3] === "import") {
1435
1579
  }
1436
1580
  })();
1437
1581
  }
1582
+ // `patchwork kill-switch engage|release|status` — issue #422 step 3.
1583
+ //
1584
+ // Discovers the running bridge via lock file, POSTs /kill-switch with
1585
+ // Bearer auth, and surfaces structured errors (env-locked, no-bridge,
1586
+ // wedged-bridge). Multi-bridge fan-out: iterates ALL live `isBridge:true`
1587
+ // locks and engages/releases each (v2-B2 from #422).
1588
+ //
1589
+ // v2-I4: mandatory 10s deadline per request. No silent fallback on
1590
+ // timeout/ECONNREFUSED/non-2xx — error message + exit non-zero.
1591
+ if (process.argv[2] === "kill-switch") {
1592
+ const sub = process.argv[3];
1593
+ if (!sub || (sub !== "engage" && sub !== "release" && sub !== "status")) {
1594
+ process.stderr.write('Usage: patchwork kill-switch <engage|release|status> [--reason "..."]\n' +
1595
+ "\n" +
1596
+ " engage Block all write-tier tool calls across every running bridge.\n" +
1597
+ " release Resume writes.\n" +
1598
+ " status Print engaged/locked state per running bridge.\n" +
1599
+ "\n" +
1600
+ "Exits non-zero if any bridge is unreachable or env-locked.\n");
1601
+ process.exit(1);
1602
+ }
1603
+ (async () => {
1604
+ try {
1605
+ // Parse optional flags early so --force-local can be used without a bridge.
1606
+ const args = process.argv.slice(4);
1607
+ const reasonIdx = args.findIndex((a) => a === "--reason" || a === "-m");
1608
+ const reason = reasonIdx >= 0 && reasonIdx + 1 < args.length
1609
+ ? args[reasonIdx + 1]
1610
+ : undefined;
1611
+ // v2-I4: --force-local writes flags.json directly when no live bridge
1612
+ // is reachable. The running bridge's fs.watch (v2-S1) picks up the
1613
+ // change within ~100ms; without a running bridge this is "effective
1614
+ // next boot" — which is still better than a silent noop.
1615
+ const forceLocal = args.includes("--force-local");
1616
+ // v2-B2: enumerate ALL live bridge locks (not just the first).
1617
+ const { findAllLiveBridges } = await import("./bridgeLockDiscovery.js");
1618
+ const liveLocks = findAllLiveBridges();
1619
+ if (liveLocks.length === 0) {
1620
+ if (forceLocal && (sub === "engage" || sub === "release")) {
1621
+ // --force-local: write flags.json directly. The running bridge's
1622
+ // fs.watch picks this up within ~100ms; if the bridge is wedged
1623
+ // or not started, this is effective on next start.
1624
+ const { setFlag, KILL_SWITCH_WRITES } = await import("./featureFlags.js");
1625
+ const engage = sub === "engage";
1626
+ setFlag(KILL_SWITCH_WRITES, engage, true);
1627
+ // Audit in a sibling CLI-only JSONL (v2-I10: bridge-only writes
1628
+ // go to decision_traces.jsonl; CLI fallback is distinct).
1629
+ const os = await import("node:os");
1630
+ const path = await import("node:path");
1631
+ const fs = await import("node:fs");
1632
+ const cliTraceFile = path.join(process.env.PATCHWORK_HOME ??
1633
+ path.join(os.default.homedir(), ".patchwork"), "decision_traces.cli.jsonl");
1634
+ const dir = path.dirname(cliTraceFile);
1635
+ if (!fs.existsSync(dir))
1636
+ fs.mkdirSync(dir, { recursive: true });
1637
+ const entry = JSON.stringify({
1638
+ ts: new Date().toISOString(),
1639
+ event: engage ? "engage" : "release",
1640
+ actor: "cli-force-local",
1641
+ ...(reason ? { reason } : {}),
1642
+ });
1643
+ fs.appendFileSync(cliTraceFile, `${entry}\n`);
1644
+ process.stdout.write(` ✓ kill-switch ${engage ? "ENGAGED" : "released"} via --force-local (flags.json written directly).\n` +
1645
+ " Running bridges will pick this up via fs.watch within ~100ms.\n");
1646
+ process.exit(0);
1647
+ }
1648
+ process.stderr.write("No running bridge found.\n" +
1649
+ " - For `engage`/`release`, kill-switch has no live target to update.\n" +
1650
+ " - Use --force-local to write flags.json directly (bridge fs.watch picks it up).\n" +
1651
+ " - Or restart the bridge and re-run this command.\n");
1652
+ process.exit(2);
1653
+ }
1654
+ // v2-I4: 10s per-request deadline. AbortController per call.
1655
+ async function callBridge(lock, method, body) {
1656
+ const controller = new AbortController();
1657
+ const timer = setTimeout(() => controller.abort(), 10_000);
1658
+ try {
1659
+ const res = await fetch(`http://127.0.0.1:${lock.port}/kill-switch`, {
1660
+ method,
1661
+ headers: {
1662
+ Authorization: `Bearer ${lock.authToken}`,
1663
+ "Content-Type": "application/json",
1664
+ },
1665
+ ...(body ? { body: JSON.stringify(body) } : {}),
1666
+ signal: controller.signal,
1667
+ });
1668
+ let json;
1669
+ try {
1670
+ json = (await res.json());
1671
+ }
1672
+ catch {
1673
+ json = undefined;
1674
+ }
1675
+ return {
1676
+ ok: res.status >= 200 && res.status < 300,
1677
+ status: res.status,
1678
+ ...(json ? { json } : {}),
1679
+ };
1680
+ }
1681
+ catch (err) {
1682
+ return {
1683
+ ok: false,
1684
+ status: 0,
1685
+ error: err instanceof Error ? err.message : String(err),
1686
+ };
1687
+ }
1688
+ finally {
1689
+ clearTimeout(timer);
1690
+ }
1691
+ }
1692
+ if (sub === "status") {
1693
+ let anyFailed = false;
1694
+ for (const lock of liveLocks) {
1695
+ const result = await callBridge(lock, "GET");
1696
+ if (!result.ok) {
1697
+ anyFailed = true;
1698
+ process.stderr.write(` ✗ bridge pid=${lock.pid} port=${lock.port} unreachable (${result.error ?? `status ${result.status}`})\n`);
1699
+ continue;
1700
+ }
1701
+ const j = result.json ?? {};
1702
+ const engaged = j.engaged === true ? "ENGAGED" : "released";
1703
+ const lockedSuffix = j.locked
1704
+ ? ` [env-locked: ${j.lockedReason ?? "yes"}]`
1705
+ : "";
1706
+ const wsLabel = lock.workspace
1707
+ ? lock.workspace.split("/").slice(-2).join("/")
1708
+ : `pid=${lock.pid}`;
1709
+ process.stdout.write(` ${engaged} port=${lock.port} ${wsLabel}${lockedSuffix}\n`);
1710
+ }
1711
+ process.exit(anyFailed ? 2 : 0);
1712
+ }
1713
+ // engage / release: POST to every live bridge, surface aggregate result.
1714
+ const engage = sub === "engage";
1715
+ let anyFailed = false;
1716
+ let anyChanged = false;
1717
+ for (const lock of liveLocks) {
1718
+ const result = await callBridge(lock, "POST", {
1719
+ engage,
1720
+ ...(reason ? { reason } : {}),
1721
+ });
1722
+ const wsLabel = lock.workspace
1723
+ ? lock.workspace.split("/").slice(-2).join("/")
1724
+ : `pid=${lock.pid}`;
1725
+ if (result.status === 409) {
1726
+ anyFailed = true;
1727
+ const lr = result.json?.lockedReason ??
1728
+ "env-locked at boot";
1729
+ process.stderr.write(` ✗ port=${lock.port} ${wsLabel}: cannot ${sub} — ${lr}\n`);
1730
+ continue;
1731
+ }
1732
+ if (!result.ok) {
1733
+ anyFailed = true;
1734
+ process.stderr.write(` ✗ port=${lock.port} ${wsLabel}: ${result.error ?? `status ${result.status}`}\n`);
1735
+ continue;
1736
+ }
1737
+ const j = result.json ?? {};
1738
+ const changedTag = j.changed === true ? "" : " (no-op, already in state)";
1739
+ if (j.changed === true)
1740
+ anyChanged = true;
1741
+ process.stdout.write(` ✓ port=${lock.port} ${wsLabel}: ${engage ? "ENGAGED" : "released"}${changedTag}\n`);
1742
+ }
1743
+ if (anyFailed) {
1744
+ process.exit(2);
1745
+ }
1746
+ if (!anyChanged) {
1747
+ process.stdout.write(`\n All ${liveLocks.length} bridge${liveLocks.length === 1 ? "" : "s"} already in target state — no audit emit.\n`);
1748
+ }
1749
+ else {
1750
+ process.stdout.write(`\n Kill-switch ${engage ? "engaged" : "released"} on ${liveLocks.length} bridge${liveLocks.length === 1 ? "" : "s"}.\n`);
1751
+ }
1752
+ process.exit(0);
1753
+ }
1754
+ catch (err) {
1755
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1756
+ process.exit(1);
1757
+ }
1758
+ })();
1759
+ }
1760
+ // `patchwork panic` — alias for `patchwork kill-switch engage` (v2-Strong-2).
1761
+ //
1762
+ // Discoverable under stress (short command, obvious intent). Canonical noun
1763
+ // form is `kill-switch engage`; this alias matches it so shell history six
1764
+ // months later still makes sense. Does not accept sub-verbs — just runs engage.
1765
+ if (process.argv[2] === "panic") {
1766
+ // Spawn self with kill-switch engage to reuse the full handler without
1767
+ // duplicating 200+ LOC. Passes through any flags (--reason, --force-local).
1768
+ import("node:child_process").then(({ spawnSync }) => {
1769
+ const self = process.argv[1] ?? process.execPath;
1770
+ const extra = process.argv.slice(3); // e.g. --reason "..." --force-local
1771
+ const result = spawnSync(process.execPath, [self, "kill-switch", "engage", ...extra], { stdio: "inherit" });
1772
+ process.exit(result.status ?? 1);
1773
+ });
1774
+ }
1775
+ // `patchwork halts` — one-screen morning summary of recent recipe halts.
1776
+ //
1777
+ // Composes the haltReason field (#441), category aggregator + endpoint
1778
+ // (#444), and dashboard pill conventions: queries the live bridge's
1779
+ // /runs/halt-summary endpoint over the chosen window and prints a
1780
+ // per-category breakdown plus the 5 most-recent halt reasons. Default
1781
+ // window is "overnight" (since 6pm yesterday local) so it lines up with
1782
+ // "what halted while I was asleep?".
1783
+ if (process.argv[2] === "halts") {
1784
+ const args = process.argv.slice(3);
1785
+ const wantHelp = args.includes("--help") || args.includes("-h");
1786
+ if (wantHelp) {
1787
+ process.stdout.write("Usage: patchwork halts [--window <name>] [--recipe <name>] [--json]\n" +
1788
+ "\n" +
1789
+ " --window 1h | 24h | overnight | 7d | any (default: overnight)\n" +
1790
+ " --recipe <name> filter to one recipe by name\n" +
1791
+ " --json emit raw JSON (for scripting)\n" +
1792
+ "\n" +
1793
+ '"overnight" = since 6pm yesterday local time.\n');
1794
+ process.exit(0);
1795
+ }
1796
+ function parseWindow() {
1797
+ const idx = args.findIndex((a) => a === "--window" || a === "-w");
1798
+ const raw = idx >= 0 && idx + 1 < args.length ? args[idx + 1] : "overnight";
1799
+ if (raw === "1h" ||
1800
+ raw === "24h" ||
1801
+ raw === "overnight" ||
1802
+ raw === "7d" ||
1803
+ raw === "any")
1804
+ return raw;
1805
+ process.stderr.write(`Unknown --window value: "${raw}"\n`);
1806
+ process.exit(1);
1807
+ }
1808
+ function windowSinceMs(w) {
1809
+ if (w === "any")
1810
+ return null;
1811
+ if (w === "1h")
1812
+ return 60 * 60 * 1000;
1813
+ if (w === "24h")
1814
+ return 24 * 60 * 60 * 1000;
1815
+ if (w === "7d")
1816
+ return 7 * 24 * 60 * 60 * 1000;
1817
+ const d = new Date();
1818
+ d.setHours(18, 0, 0, 0);
1819
+ if (d.getTime() > Date.now())
1820
+ d.setDate(d.getDate() - 1);
1821
+ return Date.now() - d.getTime();
1822
+ }
1823
+ const window = parseWindow();
1824
+ const wantJson = args.includes("--json");
1825
+ const recipeIdx = args.findIndex((a) => a === "--recipe" || a === "-r");
1826
+ const recipeFilter = recipeIdx >= 0 && recipeIdx + 1 < args.length
1827
+ ? args[recipeIdx + 1]
1828
+ : undefined;
1829
+ (async () => {
1830
+ try {
1831
+ const { findAllLiveBridges } = await import("./bridgeLockDiscovery.js");
1832
+ const liveLocks = findAllLiveBridges();
1833
+ if (liveLocks.length === 0) {
1834
+ process.stderr.write("No running bridge found. Start one with `patchwork start` (or `--driver subprocess`).\n");
1835
+ process.exit(2);
1836
+ }
1837
+ // Single-bridge default: query the first. Multi-bridge users will
1838
+ // typically have one orchestrator anyway; expanding to fan-out is a
1839
+ // follow-up if needed.
1840
+ const lock = liveLocks[0];
1841
+ if (!lock) {
1842
+ process.stderr.write("No running bridge found.\n");
1843
+ process.exit(2);
1844
+ }
1845
+ const sinceMs = windowSinceMs(window);
1846
+ const params = [];
1847
+ if (sinceMs != null)
1848
+ params.push(`sinceMs=${sinceMs}`);
1849
+ if (recipeFilter)
1850
+ params.push(`recipe=${encodeURIComponent(recipeFilter)}`);
1851
+ const qs = params.length > 0 ? `?${params.join("&")}` : "";
1852
+ const controller = new AbortController();
1853
+ const timer = setTimeout(() => controller.abort(), 10_000);
1854
+ let res;
1855
+ try {
1856
+ res = await fetch(`http://127.0.0.1:${lock.port}/runs/halt-summary${qs}`, {
1857
+ headers: { Authorization: `Bearer ${lock.authToken}` },
1858
+ signal: controller.signal,
1859
+ });
1860
+ }
1861
+ finally {
1862
+ clearTimeout(timer);
1863
+ }
1864
+ if (!res.ok) {
1865
+ process.stderr.write(`Bridge returned ${res.status} for /runs/halt-summary\n`);
1866
+ process.exit(1);
1867
+ }
1868
+ const summary = (await res.json());
1869
+ if (wantJson) {
1870
+ process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
1871
+ process.exit(0);
1872
+ }
1873
+ const labels = {
1874
+ agent_silent_fail: "agent silent-fail",
1875
+ agent_narration_only: "agent narration-only",
1876
+ agent_threw: "agent threw",
1877
+ tool_threw: "tool threw",
1878
+ tool_error: "tool error",
1879
+ kill_switch: "kill-switch blocked",
1880
+ run_level: "run-level halt",
1881
+ unknown: "uncategorised",
1882
+ };
1883
+ const windowLabel = {
1884
+ "1h": "last hour",
1885
+ "24h": "last 24h",
1886
+ overnight: "since 6pm yesterday",
1887
+ "7d": "last 7 days",
1888
+ any: "all time",
1889
+ };
1890
+ const recipeSuffix = recipeFilter ? ` · recipe="${recipeFilter}"` : "";
1891
+ process.stdout.write(`Halts — ${windowLabel[window]}${recipeSuffix}\n`);
1892
+ process.stdout.write(`Total: ${summary.total}\n`);
1893
+ if (summary.total === 0) {
1894
+ process.stdout.write("\n (nothing halted in this window)\n");
1895
+ process.exit(0);
1896
+ }
1897
+ const entries = Object.entries(summary.byCategory).sort(([, a], [, b]) => b - a);
1898
+ process.stdout.write("\nBy category:\n");
1899
+ for (const [cat, count] of entries) {
1900
+ const label = labels[cat] ?? cat;
1901
+ process.stdout.write(` ${String(count).padStart(3)} ${label}\n`);
1902
+ }
1903
+ if (summary.recent.length > 0) {
1904
+ process.stdout.write("\nMost recent:\n");
1905
+ for (const r of summary.recent) {
1906
+ // Truncate the reason to ~120 chars so a wide stack trace
1907
+ // can't blow up the terminal width on phones / narrow panes.
1908
+ const reason = r.reason.length > 120 ? `${r.reason.slice(0, 117)}…` : r.reason;
1909
+ process.stdout.write(` #${r.runSeq} [${r.category}] ${reason}\n`);
1910
+ }
1911
+ }
1912
+ process.exit(0);
1913
+ }
1914
+ catch (err) {
1915
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1916
+ process.exit(1);
1917
+ }
1918
+ })();
1919
+ }
1920
+ // `patchwork judgments` — PR3b sibling of `patchwork halts`. Same window
1921
+ // + recipe filter shape; queries /runs/judge-summary and prints a
1922
+ // per-verdict breakdown plus the 5 most-recent verdicts.
1923
+ if (process.argv[2] === "judgments") {
1924
+ const args = process.argv.slice(3);
1925
+ const wantHelp = args.includes("--help") || args.includes("-h");
1926
+ if (wantHelp) {
1927
+ process.stdout.write("Usage: patchwork judgments [--window <name>] [--recipe <name>] [--json]\n" +
1928
+ "\n" +
1929
+ " --window 1h | 24h | overnight | 7d | any (default: overnight)\n" +
1930
+ " --recipe <name> filter to one recipe by name\n" +
1931
+ " --json emit raw JSON (for scripting)\n" +
1932
+ "\n" +
1933
+ '"overnight" = since 6pm yesterday local time.\n');
1934
+ process.exit(0);
1935
+ }
1936
+ function parseWindow() {
1937
+ const idx = args.findIndex((a) => a === "--window" || a === "-w");
1938
+ const raw = idx >= 0 && idx + 1 < args.length ? args[idx + 1] : "overnight";
1939
+ if (raw === "1h" ||
1940
+ raw === "24h" ||
1941
+ raw === "overnight" ||
1942
+ raw === "7d" ||
1943
+ raw === "any")
1944
+ return raw;
1945
+ process.stderr.write(`Unknown --window value: "${raw}"\n`);
1946
+ process.exit(1);
1947
+ }
1948
+ function windowSinceMs(w) {
1949
+ if (w === "any")
1950
+ return null;
1951
+ if (w === "1h")
1952
+ return 60 * 60 * 1000;
1953
+ if (w === "24h")
1954
+ return 24 * 60 * 60 * 1000;
1955
+ if (w === "7d")
1956
+ return 7 * 24 * 60 * 60 * 1000;
1957
+ const d = new Date();
1958
+ d.setHours(18, 0, 0, 0);
1959
+ if (d.getTime() > Date.now())
1960
+ d.setDate(d.getDate() - 1);
1961
+ return Date.now() - d.getTime();
1962
+ }
1963
+ const window = parseWindow();
1964
+ const wantJson = args.includes("--json");
1965
+ const recipeIdx = args.findIndex((a) => a === "--recipe" || a === "-r");
1966
+ const recipeFilter = recipeIdx >= 0 && recipeIdx + 1 < args.length
1967
+ ? args[recipeIdx + 1]
1968
+ : undefined;
1969
+ (async () => {
1970
+ try {
1971
+ const { findAllLiveBridges } = await import("./bridgeLockDiscovery.js");
1972
+ const liveLocks = findAllLiveBridges();
1973
+ if (liveLocks.length === 0) {
1974
+ process.stderr.write("No running bridge found. Start one with `patchwork start` (or `--driver subprocess`).\n");
1975
+ process.exit(2);
1976
+ }
1977
+ const lock = liveLocks[0];
1978
+ if (!lock) {
1979
+ process.stderr.write("No running bridge found.\n");
1980
+ process.exit(2);
1981
+ }
1982
+ const sinceMs = windowSinceMs(window);
1983
+ const params = [];
1984
+ if (sinceMs != null)
1985
+ params.push(`sinceMs=${sinceMs}`);
1986
+ if (recipeFilter)
1987
+ params.push(`recipe=${encodeURIComponent(recipeFilter)}`);
1988
+ const qs = params.length > 0 ? `?${params.join("&")}` : "";
1989
+ const controller = new AbortController();
1990
+ const timer = setTimeout(() => controller.abort(), 10_000);
1991
+ let res;
1992
+ try {
1993
+ res = await fetch(`http://127.0.0.1:${lock.port}/runs/judge-summary${qs}`, {
1994
+ headers: { Authorization: `Bearer ${lock.authToken}` },
1995
+ signal: controller.signal,
1996
+ });
1997
+ }
1998
+ finally {
1999
+ clearTimeout(timer);
2000
+ }
2001
+ if (!res.ok) {
2002
+ process.stderr.write(`Bridge returned ${res.status} for /runs/judge-summary\n`);
2003
+ process.exit(1);
2004
+ }
2005
+ const summary = (await res.json());
2006
+ if (wantJson) {
2007
+ process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
2008
+ process.exit(0);
2009
+ }
2010
+ const labels = {
2011
+ approve: "approve",
2012
+ request_changes: "request changes",
2013
+ unparseable: "unparseable",
2014
+ };
2015
+ const windowLabel = {
2016
+ "1h": "last hour",
2017
+ "24h": "last 24h",
2018
+ overnight: "since 6pm yesterday",
2019
+ "7d": "last 7 days",
2020
+ any: "all time",
2021
+ };
2022
+ const recipeSuffix = recipeFilter ? ` · recipe="${recipeFilter}"` : "";
2023
+ process.stdout.write(`Judgments — ${windowLabel[window]}${recipeSuffix}\n`);
2024
+ process.stdout.write(`Total: ${summary.total}\n`);
2025
+ if (summary.total === 0) {
2026
+ process.stdout.write("\n (no judge steps fired in this window)\n");
2027
+ process.exit(0);
2028
+ }
2029
+ const entries = Object.entries(summary.byVerdict).sort(([, a], [, b]) => b - a);
2030
+ process.stdout.write("\nBy verdict:\n");
2031
+ for (const [verdict, count] of entries) {
2032
+ const label = labels[verdict] ?? verdict;
2033
+ process.stdout.write(` ${String(count).padStart(3)} ${label}\n`);
2034
+ }
2035
+ if (summary.recent.length > 0) {
2036
+ process.stdout.write("\nMost recent:\n");
2037
+ for (const r of summary.recent) {
2038
+ const reason = r.firstReason
2039
+ ? r.firstReason.length > 120
2040
+ ? `${r.firstReason.slice(0, 117)}…`
2041
+ : r.firstReason
2042
+ : "(no reason)";
2043
+ process.stdout.write(` #${r.runSeq} [${r.verdict}] ${r.stepId}: ${reason}\n`);
2044
+ }
2045
+ }
2046
+ process.exit(0);
2047
+ }
2048
+ catch (err) {
2049
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
2050
+ process.exit(1);
2051
+ }
2052
+ })();
2053
+ }
1438
2054
  if (process.argv[2] === "recipe" && process.argv[3] === "schema") {
1439
2055
  const outputDir = process.argv[4] ?? path.join(process.cwd(), "schemas");
1440
2056
  (async () => {
@@ -1454,44 +2070,62 @@ if (process.argv[2] === "recipe" && process.argv[3] === "schema") {
1454
2070
  })();
1455
2071
  }
1456
2072
  // Patchwork: `patchwork recipe new <name>` — scaffold a new recipe from template.
2073
+ // With `--interactive`, drops into a connector-aware prompt tree instead.
1457
2074
  if (process.argv[2] === "recipe" && process.argv[3] === "new") {
1458
2075
  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 {
2076
+ const isInteractive = args.includes("--interactive") || args.includes("-i");
2077
+ if (isInteractive) {
1475
2078
  (async () => {
1476
2079
  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}`;
2080
+ const { runNewInteractive } = await import("./commands/recipe.js");
2081
+ const { createInterface } = await import("node:readline/promises");
2082
+ const rl = createInterface({
2083
+ input: process.stdin,
2084
+ output: process.stdout,
2085
+ });
1483
2086
  const outIdx = args.indexOf("--out");
1484
2087
  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
2088
  const outputDir = outRaw ? path.resolve(outRaw) : undefined;
1488
- const result = runNew({
1489
- name: recipeName,
1490
- description,
1491
- ...(template ? { template } : {}),
2089
+ const deps = {
2090
+ ask: async (q) => (await rl.question(`${q}: `)).trim(),
2091
+ pickFromList: async (q, options) => {
2092
+ process.stdout.write(`\n${q}\n`);
2093
+ options.forEach((opt, i) => {
2094
+ process.stdout.write(` ${i + 1}. ${opt}\n`);
2095
+ });
2096
+ for (let attempt = 0; attempt < 5; attempt++) {
2097
+ const raw = (await rl.question(`Choose 1-${options.length}: `)).trim();
2098
+ const idx = Number.parseInt(raw, 10);
2099
+ if (Number.isFinite(idx) && idx >= 1 && idx <= options.length) {
2100
+ return idx;
2101
+ }
2102
+ process.stdout.write(`Invalid choice. Enter a number 1-${options.length}.\n`);
2103
+ }
2104
+ throw new Error("Too many invalid choices");
2105
+ },
2106
+ confirm: async (q) => {
2107
+ const a = (await rl.question(`${q} [y/N]: `)).trim().toLowerCase();
2108
+ return a === "y" || a === "yes";
2109
+ },
2110
+ preview: (yaml) => {
2111
+ process.stdout.write("\n--- Preview ---\n");
2112
+ process.stdout.write(yaml);
2113
+ process.stdout.write("---\n\n");
2114
+ },
2115
+ };
2116
+ const result = await runNewInteractive({
2117
+ deps,
1492
2118
  ...(outputDir ? { outputDir } : {}),
1493
2119
  });
2120
+ rl.close();
1494
2121
  process.stdout.write(` ✓ Created ${result.path}\n`);
2122
+ if (result.warnings.length > 0) {
2123
+ process.stdout.write(`\n ⚠ Lint warnings (recipe still written):\n`);
2124
+ for (const w of result.warnings) {
2125
+ process.stdout.write(` [${w.level}] ${w.message}\n`);
2126
+ }
2127
+ }
2128
+ process.stdout.write(`\n Run with: patchwork recipe run ${result.path}\n`);
1495
2129
  process.exit(0);
1496
2130
  }
1497
2131
  catch (err) {
@@ -1500,6 +2134,53 @@ if (process.argv[2] === "recipe" && process.argv[3] === "new") {
1500
2134
  }
1501
2135
  })();
1502
2136
  }
2137
+ else {
2138
+ const recipeName = args[0];
2139
+ if (!recipeName) {
2140
+ process.stderr.write("Usage: patchwork recipe new <name> [--template <name>] [--desc <description>] [--out <dir>]\n" +
2141
+ " --interactive (-i) Run the connector-aware prompt tree instead of using a template.\n" +
2142
+ " --out <dir> Write the recipe to <dir>/<name>.yaml.\n" +
2143
+ " Defaults to ~/.patchwork/recipes/ — pass `--out .` to\n" +
2144
+ " write into the current directory instead.\n");
2145
+ process.stderr.write("\nTemplates:\n");
2146
+ (async () => {
2147
+ const { listTemplates } = await import("./commands/recipe.js");
2148
+ for (const t of listTemplates()) {
2149
+ process.stderr.write(` ${t}\n`);
2150
+ }
2151
+ process.exit(1);
2152
+ })();
2153
+ }
2154
+ else {
2155
+ (async () => {
2156
+ try {
2157
+ const { runNew } = await import("./commands/recipe.js");
2158
+ const templateIdx = args.indexOf("--template");
2159
+ const template = templateIdx >= 0 ? args[templateIdx + 1] : undefined;
2160
+ const descIdx = args.indexOf("--desc");
2161
+ const description = (descIdx >= 0 ? args[descIdx + 1] : undefined) ??
2162
+ `Recipe: ${recipeName}`;
2163
+ const outIdx = args.indexOf("--out");
2164
+ const outRaw = outIdx >= 0 ? args[outIdx + 1] : undefined;
2165
+ // `--out .` is the common case for "scaffold in cwd" — resolve so
2166
+ // the success message shows the absolute path the user can open.
2167
+ const outputDir = outRaw ? path.resolve(outRaw) : undefined;
2168
+ const result = runNew({
2169
+ name: recipeName,
2170
+ description,
2171
+ ...(template ? { template } : {}),
2172
+ ...(outputDir ? { outputDir } : {}),
2173
+ });
2174
+ process.stdout.write(` ✓ Created ${result.path}\n`);
2175
+ process.exit(0);
2176
+ }
2177
+ catch (err) {
2178
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
2179
+ process.exit(1);
2180
+ }
2181
+ })();
2182
+ }
2183
+ }
1503
2184
  }
1504
2185
  // Patchwork: `patchwork recipe lint <file.yaml>` — validate recipe against schema.
1505
2186
  if (process.argv[2] === "recipe" && process.argv[3] === "lint") {
@@ -1952,9 +2633,12 @@ Steps performed:
1952
2633
  }
1953
2634
  if (extensionArg2) {
1954
2635
  try {
2636
+ // Windows: editor binaries (code/cursor/windsurf) are `.cmd` shims that
2637
+ // Node's execFileSync can't launch without a shell. See bridgeProcess.ts.
1955
2638
  execFileSync(editor, ["--install-extension", extensionArg2], {
1956
2639
  stdio: "pipe",
1957
2640
  timeout: 30000,
2641
+ shell: process.platform === "win32",
1958
2642
  });
1959
2643
  process.stderr.write(` ✓ Extension installed via ${editor}\n\n`);
1960
2644
  }
@@ -2083,13 +2767,18 @@ Steps performed:
2083
2767
  process.stderr.write(` ✓ MCP shim — already registered in ${claudeJsonAbs}\n\n`);
2084
2768
  }
2085
2769
  else {
2770
+ // claude -p spawns the stdio command via Node's child_process, which
2771
+ // can't resolve a bare `.cmd` shim on Windows. Record the `.cmd` form
2772
+ // on win32 so the bridge binary is findable by the spawned process.
2086
2773
  mcpServers["claude-ide-bridge"] = {
2087
- command: "claude-ide-bridge",
2774
+ command: ensureCmdShim("claude-ide-bridge"),
2088
2775
  args: ["shim"],
2089
2776
  type: "stdio",
2090
2777
  };
2091
2778
  claudeJson.mcpServers = mcpServers;
2092
- writeFileSync(claudeJsonAbs, `${JSON.stringify(claudeJson, null, 2)}\n`);
2779
+ // Atomic — `~/.claude.json` holds every MCP server registration on
2780
+ // the machine. A crash mid-write would brick Claude Code globally.
2781
+ writeFileAtomicSync(claudeJsonAbs, `${JSON.stringify(claudeJson, null, 2)}\n`);
2093
2782
  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
2783
  }
2095
2784
  }
@@ -2140,7 +2829,9 @@ Steps performed:
2140
2829
  }
2141
2830
  if (added.length > 0 || migrated.length > 0) {
2142
2831
  ccSettings.hooks = ccHooks;
2143
- writeFileSync(ccSettingsPath, `${JSON.stringify(ccSettings, null, 2)}\n`);
2832
+ // Atomic — `~/.claude/settings.json` holds every CC hook entry; a
2833
+ // crash mid-write loses the user's full hook configuration.
2834
+ writeFileAtomicSync(ccSettingsPath, `${JSON.stringify(ccSettings, null, 2)}\n`);
2144
2835
  const addMsg = added.length > 0
2145
2836
  ? ` ✓ CC hooks — wired ${added.length} automation hook(s) in ${ccSettingsPath}\n Added: ${added.join(", ")}\n`
2146
2837
  : "";
@@ -2181,6 +2872,9 @@ Steps performed:
2181
2872
  execFileSync("claude-ide-bridge", ["--version"], {
2182
2873
  stdio: "pipe",
2183
2874
  timeout: 5000,
2875
+ // Windows: global npm bin is a `.cmd` shim that Node's execFileSync
2876
+ // can't launch without a shell. See bridgeProcess.ts for context.
2877
+ shell: process.platform === "win32",
2184
2878
  });
2185
2879
  shimOnPath = true;
2186
2880
  }
@@ -2240,13 +2934,8 @@ Steps performed:
2240
2934
  let hooksWired = false;
2241
2935
  try {
2242
2936
  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
- }
2937
+ const { isPreToolUseHookRegistered } = await import("./preToolUseHook.js");
2938
+ hooksWired = isPreToolUseHookRegistered(settingsPath);
2250
2939
  }
2251
2940
  catch {
2252
2941
  /* file may not exist yet — non-fatal */
@@ -2264,8 +2953,11 @@ Steps performed:
2264
2953
  ? fallbackDocs
2265
2954
  : null;
2266
2955
  if (target) {
2267
- const { exec } = await import("node:child_process");
2268
- exec(`code "${target}"`, { timeout: 3000 }, () => { });
2956
+ // Use execFile with argv (no shell) — exec(`code "${target}"`) was
2957
+ // shell-evaluated and could be injected via `--workspace '"; ...'`
2958
+ // since path.resolve preserves shell metachars. Audit 2026-05-17.
2959
+ const { execFile } = await import("node:child_process");
2960
+ execFile("code", [target], { timeout: 3000 }, () => { });
2269
2961
  }
2270
2962
  }
2271
2963
  // Analytics opt-in prompt — only ask once; skip if preference already set
@@ -2382,9 +3074,11 @@ else if (process.argv[2] === "install-extension") {
2382
3074
  }
2383
3075
  try {
2384
3076
  process.stderr.write(`Installing extension via ${editor}...\n`);
3077
+ // Windows: editor binaries are `.cmd` shims; need shell for resolution.
2385
3078
  execFileSync(editor, ["--install-extension", extensionArg], {
2386
3079
  stdio: "inherit",
2387
3080
  timeout: 30000,
3081
+ shell: process.platform === "win32",
2388
3082
  });
2389
3083
  process.stderr.write("Extension installed successfully.\n");
2390
3084
  }
@@ -2581,11 +3275,24 @@ if (process.argv[2] === "launchd") {
2581
3275
  // F6: "Did you mean?" for unknown CLI subcommands
2582
3276
  // Patchwork: no-args → terminal dashboard (when invoked as patchwork-os or patchwork).
2583
3277
  {
2584
- const binName = path.basename(process.argv[1] ?? "");
3278
+ const binName = invokedBinaryName();
2585
3279
  const isPatchworkBin = binName === "patchwork-os" ||
2586
3280
  binName === "patchwork" ||
2587
3281
  binName === "patchwork.js";
2588
3282
  if (isPatchworkBin && (!process.argv[2] || process.argv[2] === "dashboard")) {
3283
+ // First-run guard: if the user hasn't run `patchwork init` yet, launching
3284
+ // the dashboard renders an empty panel with no signpost. Print an
3285
+ // actionable pointer instead and exit cleanly.
3286
+ const cfgPath = path.join(os.homedir(), ".patchwork", "config.json");
3287
+ if (!existsSync(cfgPath) && !process.argv[2]) {
3288
+ process.stdout.write(`No Patchwork config found at ${cfgPath}.\n\n` +
3289
+ `Run \`${binName} init\` to scaffold ~/.patchwork and wire up\n` +
3290
+ `Claude Code hooks, then \`${binName}\` again to open the dashboard.\n\n` +
3291
+ `For just the IDE bridge (no recipes / approval queue), run:\n` +
3292
+ ` ${binName} install-extension\n` +
3293
+ ` ${binName} --workspace .\n`);
3294
+ process.exit(0);
3295
+ }
2589
3296
  (async () => {
2590
3297
  const { runDashboard } = await import("./commands/dashboard.js");
2591
3298
  await runDashboard();
@@ -2663,8 +3370,10 @@ else {
2663
3370
  .digest("hex")
2664
3371
  .slice(0, 6);
2665
3372
  const sessionName = `claude-bridge-${ws}${hash}`;
2666
- // Check if tmux is available
2667
- const tmuxCheck = spawnSync("which", ["tmux"], { stdio: "ignore" });
3373
+ // Check if tmux is available (skip on Windows — tmux doesn't exist there)
3374
+ const tmuxCheck = process.platform !== "win32"
3375
+ ? spawnSync("which", ["tmux"], { stdio: "ignore" })
3376
+ : { status: 1 };
2668
3377
  if (tmuxCheck.status !== 0) {
2669
3378
  process.stderr.write("WARNING: --auto-tmux requested but tmux is not installed. Running without tmux.\n");
2670
3379
  }
@@ -2713,7 +3422,12 @@ else {
2713
3422
  for (const sig of ["SIGTERM", "SIGINT"]) {
2714
3423
  process.once(sig, () => {
2715
3424
  stopping = true;
2716
- child.kill(sig);
3425
+ // Use treeKill so grandchildren (recipe runners, claude
3426
+ // subprocesses, extension watchers) are reaped on Windows.
3427
+ // Bare `child.kill(sig)` maps to TerminateProcess on win32
3428
+ // and skips descendants → orphaned processes survive a
3429
+ // supervisor SIGTERM. Audit 2026-05-17.
3430
+ treeKill(child, sig);
2717
3431
  });
2718
3432
  }
2719
3433
  child.on("exit", (code, signal) => {
@@ -2741,19 +3455,27 @@ else {
2741
3455
  process.stderr.write(`Error: ${message}\n`);
2742
3456
  process.exit(1);
2743
3457
  });
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(() => { });
3458
+ // F5: Silent self-update nudge (fire-and-forget).
3459
+ // Skip when running from a source tree (any of: a `.git` sibling of the
3460
+ // package, or __dirnameTop not under a node_modules/). Otherwise a dev
3461
+ // who built locally sees "Bridge v<X> available" pointing at an npm
3462
+ // install path they're not using.
3463
+ const isSourceBuild = existsSync(path.join(__dirnameTop, "..", ".git")) ||
3464
+ !__dirnameTop.includes(`${path.sep}node_modules${path.sep}`);
3465
+ if (!isSourceBuild) {
3466
+ import("node:child_process")
3467
+ .then(({ exec }) => {
3468
+ exec("npm view patchwork-os version", { timeout: 5000 }, (err, stdout) => {
3469
+ if (err || !stdout)
3470
+ return;
3471
+ const latest = stdout.trim();
3472
+ if (latest && semverGt(latest, PACKAGE_VERSION)) {
3473
+ console.log(`\n Patchwork OS v${latest} available — run: npm update -g patchwork-os\n`);
3474
+ }
3475
+ });
3476
+ })
3477
+ .catch(() => { });
3478
+ }
2757
3479
  }
2758
3480
  } // end of `else` for `if (__subcommandWillRun)` (bridge-mode block)
2759
3481
  //# sourceMappingURL=index.js.map