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
@@ -51,8 +51,53 @@ export declare function _resetEnvLockForTesting(): void;
51
51
  * 3. Default value from registration
52
52
  */
53
53
  export declare function isEnabled(flagId: string): boolean;
54
+ /**
55
+ * Returns true when the given kill-switch flag is **env-locked** — i.e. its
56
+ * value was frozen at startup from a `PATCHWORK_FLAG_<ID>` environment
57
+ * variable and any subsequent `setFlag()` call will be silently overridden
58
+ * by `isEnabled()`.
59
+ *
60
+ * Used by the `/kill-switch` endpoint (issue #422) to surface a 409
61
+ * Conflict instead of returning 200 OK for a setFlag that won't stick,
62
+ * and by the dashboard to render the toggle as disabled (with a tooltip
63
+ * naming which direction was sysadmin-locked) when this returns true.
64
+ *
65
+ * Returns false for non-kill-switch flags (they read env dynamically and
66
+ * are never "locked"), for unknown flags, and when `lockKillSwitchEnv()`
67
+ * has not yet been called.
68
+ */
69
+ export declare function isEnvLockedFor(flagId: string): boolean;
70
+ /**
71
+ * Returns the frozen env-locked value for a kill-switch flag (`true` /
72
+ * `false`), or `null` if not env-locked.
73
+ *
74
+ * Used by the dashboard tooltip so the disabled-state can read "env-locked
75
+ * to **on**" vs "env-locked to **off**" — both directions are policy-locked,
76
+ * but the user should know which.
77
+ */
78
+ export declare function getEnvLockedValue(flagId: string): boolean | null;
79
+ /**
80
+ * Thrown by `setFlag` when a kill-switch flag is env-locked (its value was
81
+ * frozen at startup via `PATCHWORK_FLAG_<ID>`) and the caller attempts to
82
+ * mutate it. The `/kill-switch` POST handler catches this and returns a
83
+ * structured 409 Conflict so the CLI / dashboard can distinguish
84
+ * "policy-locked" from "bad request" (issue #422, pitfall I9).
85
+ *
86
+ * `frozenValue` is the locked direction — callers can surface
87
+ * "env-locked to on" vs "env-locked to off" in error messages.
88
+ */
89
+ export declare class EnvLockedFlagError extends Error {
90
+ readonly flagId: string;
91
+ readonly frozenValue: boolean;
92
+ constructor(flagId: string, frozenValue: boolean);
93
+ }
54
94
  /**
55
95
  * Set a flag value (persists to config file if persist=true).
96
+ *
97
+ * Throws `EnvLockedFlagError` if the flag is a kill-switch flag that has been
98
+ * frozen by `lockKillSwitchEnv()` — a sysadmin env-var override takes
99
+ * precedence over runtime mutation. The `/kill-switch` POST handler catches
100
+ * this and returns 409 (pitfall I9 from issue #422).
56
101
  */
57
102
  export declare function setFlag(flagId: string, value: boolean, persist?: boolean): void;
58
103
  /**
@@ -69,11 +114,42 @@ export declare function loadFlags(): void;
69
114
  export declare const KILL_SWITCH_WRITES = "kill-switch.writes";
70
115
  /** Enable recipe lint with schema validation (A1) */
71
116
  export declare const FLAG_SCHEMA_LINT = "ui.schema-lint";
117
+ /**
118
+ * Watch the flags.json file for cross-process changes. When another
119
+ * process (typically the `patchwork kill-switch` CLI in its fallback
120
+ * fs-write path, or a sibling bridge in a multi-bridge deployment)
121
+ * writes to flags.json, this watcher reloads the in-memory FLAG_VALUES
122
+ * so the running bridge picks up the new state without a restart.
123
+ *
124
+ * v2-S1 + v2-B2 from #422. Closes the "no bridge reachable → CLI
125
+ * silent fallback → recipes keep writing" gap that motivated the
126
+ * redesign — even when the CLI's HTTP path fails and it falls back
127
+ * to writing the file directly, the running bridge still sees the
128
+ * change.
129
+ *
130
+ * **Env-lock interaction:** if `lockKillSwitchEnv()` already froze a
131
+ * kill-switch value from `PATCHWORK_FLAG_*`, the file-watch flow
132
+ * still updates FLAG_VALUES, but `isEnabled` continues reading from
133
+ * the frozen snapshot for that flag (existing behavior at L117-121).
134
+ * The env-lock is the source of truth — file changes can't override
135
+ * a sysadmin-mandated kill-switch state. This is the correct policy.
136
+ *
137
+ * Modeled on `src/pluginWatcher.ts`: directory-watch + filename
138
+ * filter + 100ms debounce so coalesced events (rename+create+change
139
+ * on most filesystems) don't trigger N reloads. Returns a close
140
+ * handle. Tolerates the file or directory not yet existing.
141
+ */
142
+ export declare function watchFlags(): () => void;
72
143
  /**
73
144
  * Check if write operations are globally disabled.
74
145
  */
75
146
  export declare function isWriteKillSwitchActive(): boolean;
76
147
  /**
77
148
  * Assert write is allowed — throws if kill switch is active.
149
+ *
150
+ * The thrown error carries `code: "kill_switch_blocked"` so the recipe
151
+ * runner can categorise the resulting halt as `kill_switch` (rather than
152
+ * a generic `tool_threw`) and the dashboard pill row can flag it
153
+ * distinctly from real tool failures.
78
154
  */
79
155
  export declare function assertWriteAllowed(operation: string): void;
@@ -7,9 +7,11 @@
7
7
  * - Kill switch for write-tier operations
8
8
  * - Per-feature opt-in with default-off safety
9
9
  */
10
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
11
11
  import os from "node:os";
12
12
  import { join } from "node:path";
13
+ import { watchDirectoryWithFallback } from "./fsWatchWithFallback.js";
14
+ import { writeFileAtomicSync } from "./writeFileAtomic.js";
13
15
  /** Flag registry — all known flags */
14
16
  const FLAG_REGISTRY = new Map();
15
17
  /** Runtime flag values (after env/file resolution) */
@@ -101,13 +103,85 @@ export function isEnabled(flagId) {
101
103
  // Unknown flag — default to false (safe)
102
104
  return false;
103
105
  }
106
+ /**
107
+ * Returns true when the given kill-switch flag is **env-locked** — i.e. its
108
+ * value was frozen at startup from a `PATCHWORK_FLAG_<ID>` environment
109
+ * variable and any subsequent `setFlag()` call will be silently overridden
110
+ * by `isEnabled()`.
111
+ *
112
+ * Used by the `/kill-switch` endpoint (issue #422) to surface a 409
113
+ * Conflict instead of returning 200 OK for a setFlag that won't stick,
114
+ * and by the dashboard to render the toggle as disabled (with a tooltip
115
+ * naming which direction was sysadmin-locked) when this returns true.
116
+ *
117
+ * Returns false for non-kill-switch flags (they read env dynamically and
118
+ * are never "locked"), for unknown flags, and when `lockKillSwitchEnv()`
119
+ * has not yet been called.
120
+ */
121
+ export function isEnvLockedFor(flagId) {
122
+ if (!envLocked)
123
+ return false;
124
+ const flag = FLAG_REGISTRY.get(flagId);
125
+ if (!flag?.isKillSwitch)
126
+ return false;
127
+ return FROZEN_KILL_SWITCH_ENV.get(flagId) !== undefined;
128
+ }
129
+ /**
130
+ * Returns the frozen env-locked value for a kill-switch flag (`true` /
131
+ * `false`), or `null` if not env-locked.
132
+ *
133
+ * Used by the dashboard tooltip so the disabled-state can read "env-locked
134
+ * to **on**" vs "env-locked to **off**" — both directions are policy-locked,
135
+ * but the user should know which.
136
+ */
137
+ export function getEnvLockedValue(flagId) {
138
+ if (!isEnvLockedFor(flagId))
139
+ return null;
140
+ const frozen = FROZEN_KILL_SWITCH_ENV.get(flagId);
141
+ return frozen === undefined ? null : frozen;
142
+ }
143
+ /**
144
+ * Thrown by `setFlag` when a kill-switch flag is env-locked (its value was
145
+ * frozen at startup via `PATCHWORK_FLAG_<ID>`) and the caller attempts to
146
+ * mutate it. The `/kill-switch` POST handler catches this and returns a
147
+ * structured 409 Conflict so the CLI / dashboard can distinguish
148
+ * "policy-locked" from "bad request" (issue #422, pitfall I9).
149
+ *
150
+ * `frozenValue` is the locked direction — callers can surface
151
+ * "env-locked to on" vs "env-locked to off" in error messages.
152
+ */
153
+ export class EnvLockedFlagError extends Error {
154
+ flagId;
155
+ frozenValue;
156
+ constructor(flagId, frozenValue) {
157
+ super(`Feature flag "${flagId}" is env-locked to ${frozenValue ? "true" : "false"} ` +
158
+ `(set at startup via PATCHWORK_FLAG_${flagId.replace(/[.-]/g, "_").toUpperCase()}). ` +
159
+ `Unset the environment variable to allow runtime mutation.`);
160
+ this.name = "EnvLockedFlagError";
161
+ this.flagId = flagId;
162
+ this.frozenValue = frozenValue;
163
+ }
164
+ }
104
165
  /**
105
166
  * Set a flag value (persists to config file if persist=true).
167
+ *
168
+ * Throws `EnvLockedFlagError` if the flag is a kill-switch flag that has been
169
+ * frozen by `lockKillSwitchEnv()` — a sysadmin env-var override takes
170
+ * precedence over runtime mutation. The `/kill-switch` POST handler catches
171
+ * this and returns 409 (pitfall I9 from issue #422).
106
172
  */
107
173
  export function setFlag(flagId, value, persist = false) {
108
174
  if (!FLAG_REGISTRY.has(flagId)) {
109
175
  throw new Error(`Unknown feature flag: "${flagId}"`);
110
176
  }
177
+ // v2-I9: kill-switch flags that were env-locked at startup must not be
178
+ // silently overridden — throw so callers (the /kill-switch handler) can
179
+ // surface a structured 409 instead of returning 200 for a mutation that
180
+ // won't take effect.
181
+ if (isEnvLockedFor(flagId)) {
182
+ const frozen = FROZEN_KILL_SWITCH_ENV.get(flagId);
183
+ throw new EnvLockedFlagError(flagId, frozen === true);
184
+ }
111
185
  FLAG_VALUES.set(flagId, value);
112
186
  if (persist) {
113
187
  persistFlags();
@@ -160,7 +234,7 @@ function persistFlags() {
160
234
  toSave[id] = value;
161
235
  }
162
236
  }
163
- writeFileSync(path, JSON.stringify(toSave, null, 2));
237
+ writeFileAtomicSync(path, JSON.stringify(toSave, null, 2));
164
238
  }
165
239
  // ============================================================================
166
240
  // Built-in Flags
@@ -187,6 +261,75 @@ registerFlag({
187
261
  });
188
262
  // Load persisted flags on module init
189
263
  loadFlags();
264
+ /**
265
+ * Watch the flags.json file for cross-process changes. When another
266
+ * process (typically the `patchwork kill-switch` CLI in its fallback
267
+ * fs-write path, or a sibling bridge in a multi-bridge deployment)
268
+ * writes to flags.json, this watcher reloads the in-memory FLAG_VALUES
269
+ * so the running bridge picks up the new state without a restart.
270
+ *
271
+ * v2-S1 + v2-B2 from #422. Closes the "no bridge reachable → CLI
272
+ * silent fallback → recipes keep writing" gap that motivated the
273
+ * redesign — even when the CLI's HTTP path fails and it falls back
274
+ * to writing the file directly, the running bridge still sees the
275
+ * change.
276
+ *
277
+ * **Env-lock interaction:** if `lockKillSwitchEnv()` already froze a
278
+ * kill-switch value from `PATCHWORK_FLAG_*`, the file-watch flow
279
+ * still updates FLAG_VALUES, but `isEnabled` continues reading from
280
+ * the frozen snapshot for that flag (existing behavior at L117-121).
281
+ * The env-lock is the source of truth — file changes can't override
282
+ * a sysadmin-mandated kill-switch state. This is the correct policy.
283
+ *
284
+ * Modeled on `src/pluginWatcher.ts`: directory-watch + filename
285
+ * filter + 100ms debounce so coalesced events (rename+create+change
286
+ * on most filesystems) don't trigger N reloads. Returns a close
287
+ * handle. Tolerates the file or directory not yet existing.
288
+ */
289
+ export function watchFlags() {
290
+ const flagsPath = getFlagsPath();
291
+ const flagsDir = join(flagsPath, "..");
292
+ let debounceTimer = null;
293
+ let stopped = false;
294
+ const reload = () => {
295
+ if (debounceTimer)
296
+ clearTimeout(debounceTimer);
297
+ debounceTimer = setTimeout(() => {
298
+ debounceTimer = null;
299
+ if (stopped)
300
+ return;
301
+ try {
302
+ loadFlags();
303
+ }
304
+ catch {
305
+ // loadFlags has its own try/catch for parse errors;
306
+ // this catch is belt-and-suspenders for fs errors.
307
+ }
308
+ }, 100);
309
+ };
310
+ // Watch the directory rather than the file directly — flags.json may not
311
+ // exist yet when watch is established, and editors / atomic writes
312
+ // (rename-into-place) lose direct file watches. The helper falls back to
313
+ // mtime polling when the dir is missing or fs.watch fails (Windows
314
+ // network drives, WSL bind mounts), and notices when the dir later appears.
315
+ const stopWatcher = watchDirectoryWithFallback(flagsDir, () => {
316
+ if (!stopped)
317
+ reload();
318
+ });
319
+ return () => {
320
+ stopped = true;
321
+ if (debounceTimer) {
322
+ clearTimeout(debounceTimer);
323
+ debounceTimer = null;
324
+ }
325
+ try {
326
+ stopWatcher();
327
+ }
328
+ catch {
329
+ /* ignore */
330
+ }
331
+ };
332
+ }
190
333
  // ============================================================================
191
334
  // Kill Switch Helpers
192
335
  // ============================================================================
@@ -198,11 +341,18 @@ export function isWriteKillSwitchActive() {
198
341
  }
199
342
  /**
200
343
  * Assert write is allowed — throws if kill switch is active.
344
+ *
345
+ * The thrown error carries `code: "kill_switch_blocked"` so the recipe
346
+ * runner can categorise the resulting halt as `kill_switch` (rather than
347
+ * a generic `tool_threw`) and the dashboard pill row can flag it
348
+ * distinctly from real tool failures.
201
349
  */
202
350
  export function assertWriteAllowed(operation) {
203
351
  if (isWriteKillSwitchActive()) {
204
- throw new Error(`Write operation blocked by kill switch: ${operation}. ` +
352
+ const err = new Error(`Write operation blocked by kill switch: ${operation}. ` +
205
353
  `Unset PATCHWORK_FLAG_KILL_SWITCH_WRITES or set kill-switch.writes=false to restore.`);
354
+ err.code = "kill_switch_blocked";
355
+ throw err;
206
356
  }
207
357
  }
208
358
  //# sourceMappingURL=featureFlags.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"featureFlags.js","sourceRoot":"","sources":["../src/featureFlags.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"featureFlags.js","sourceRoot":"","sources":["../src/featureFlags.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Tiny cross-process file mutex for synchronous critical sections.
3
+ *
4
+ * Used to wrap append-only JSONL writers (`decisionTraceLog`,
5
+ * `runLog`, `commitIssueLinkLog`) so two bridge processes sharing
6
+ * one `~/.claude/ide/` log directory can't interleave bytes within a
7
+ * single row.
8
+ *
9
+ * Why ad-hoc instead of `proper-lockfile`? proper-lockfile is the more
10
+ * featureful option (TTL refresh, retry jitter, async API) but adds a
11
+ * runtime dependency for a use case the bridge needs only synchronously
12
+ * in three call-sites. The pattern below — `openSync(file, "wx")` as
13
+ * the atomic lock primitive plus a stale-lock cleanup heuristic — is
14
+ * standard for shell-style flock without a native binding.
15
+ *
16
+ * ## Semantics
17
+ *
18
+ * `withFileLockSync(file, fn)`:
19
+ *
20
+ * 1. Attempts to create `${file}.lock` with `O_EXCL` (`flag: "wx"`).
21
+ * First writer wins atomically — the kernel rejects EEXIST for
22
+ * every other contender.
23
+ * 2. On EEXIST, polls with a short busy-wait (`Atomics.wait` would
24
+ * need a shared SharedArrayBuffer across processes — out of scope;
25
+ * hot-loop with `process.hrtime` instead, kernels schedule sleep
26
+ * anyway under contention).
27
+ * 3. If the existing lock is older than `staleLockMs` (default 30s),
28
+ * assumes the owner crashed and force-unlinks it before retrying.
29
+ * The append-only JSONL writes the bridge does complete in <1ms in
30
+ * practice, so 30s is generous.
31
+ * 4. Runs `fn()`, unlinks the lock in `finally`. Rethrows fn's
32
+ * exception.
33
+ *
34
+ * Lock granularity is per-file (each log gets its own lock). At the
35
+ * bridge's write volume (≤ tens/min across all three logs) contention
36
+ * is effectively zero.
37
+ *
38
+ * ## Failure modes
39
+ *
40
+ * - **Deadlock-on-crash:** mitigated by the 30s stale-lock TTL. A bridge
41
+ * that crashes mid-append leaves the lock until the next contender
42
+ * notices it's stale and clears it. The first crash is invisible
43
+ * (target file already written); subsequent contenders pay a 30s
44
+ * penalty only if they happen to arrive within the window.
45
+ * - **TOCTOU on stale-lock unlink:** two processes both decide the lock
46
+ * is stale and both `unlinkSync`. Second `unlinkSync` ENOENT is
47
+ * swallowed. Both then race the `openSync("wx")` — one wins, the
48
+ * other goes back to polling. No data corruption.
49
+ * - **NFS / non-POSIX FS:** O_EXCL is undefined on stale NFSv2; modern
50
+ * NFS (≥ v3 with `noac`) and all local FS we ship on (apfs / ext4 /
51
+ * ntfs) implement it correctly.
52
+ *
53
+ * @param file - The target file the caller is about to write. The lock
54
+ * sentinel is `${file}.lock`.
55
+ * @param fn - Synchronous critical section. Holds the lock for its
56
+ * entire duration.
57
+ * @param opts.timeoutMs - Max wait for the lock before throwing (default
58
+ * 5000). Use a small value — appends finish in <1ms in practice; a
59
+ * timeout this far above the expected duration only fires if
60
+ * something genuinely wrong happens.
61
+ * @param opts.staleLockMs - Age (ms) at which an existing lock is
62
+ * considered abandoned and force-unlinked. Default 30000.
63
+ */
64
+ export declare function withFileLockSync<T>(file: string, fn: () => T, opts?: {
65
+ timeoutMs?: number;
66
+ staleLockMs?: number;
67
+ }): T;
@@ -0,0 +1,126 @@
1
+ import { closeSync, openSync, statSync, unlinkSync } from "node:fs";
2
+ /**
3
+ * Tiny cross-process file mutex for synchronous critical sections.
4
+ *
5
+ * Used to wrap append-only JSONL writers (`decisionTraceLog`,
6
+ * `runLog`, `commitIssueLinkLog`) so two bridge processes sharing
7
+ * one `~/.claude/ide/` log directory can't interleave bytes within a
8
+ * single row.
9
+ *
10
+ * Why ad-hoc instead of `proper-lockfile`? proper-lockfile is the more
11
+ * featureful option (TTL refresh, retry jitter, async API) but adds a
12
+ * runtime dependency for a use case the bridge needs only synchronously
13
+ * in three call-sites. The pattern below — `openSync(file, "wx")` as
14
+ * the atomic lock primitive plus a stale-lock cleanup heuristic — is
15
+ * standard for shell-style flock without a native binding.
16
+ *
17
+ * ## Semantics
18
+ *
19
+ * `withFileLockSync(file, fn)`:
20
+ *
21
+ * 1. Attempts to create `${file}.lock` with `O_EXCL` (`flag: "wx"`).
22
+ * First writer wins atomically — the kernel rejects EEXIST for
23
+ * every other contender.
24
+ * 2. On EEXIST, polls with a short busy-wait (`Atomics.wait` would
25
+ * need a shared SharedArrayBuffer across processes — out of scope;
26
+ * hot-loop with `process.hrtime` instead, kernels schedule sleep
27
+ * anyway under contention).
28
+ * 3. If the existing lock is older than `staleLockMs` (default 30s),
29
+ * assumes the owner crashed and force-unlinks it before retrying.
30
+ * The append-only JSONL writes the bridge does complete in <1ms in
31
+ * practice, so 30s is generous.
32
+ * 4. Runs `fn()`, unlinks the lock in `finally`. Rethrows fn's
33
+ * exception.
34
+ *
35
+ * Lock granularity is per-file (each log gets its own lock). At the
36
+ * bridge's write volume (≤ tens/min across all three logs) contention
37
+ * is effectively zero.
38
+ *
39
+ * ## Failure modes
40
+ *
41
+ * - **Deadlock-on-crash:** mitigated by the 30s stale-lock TTL. A bridge
42
+ * that crashes mid-append leaves the lock until the next contender
43
+ * notices it's stale and clears it. The first crash is invisible
44
+ * (target file already written); subsequent contenders pay a 30s
45
+ * penalty only if they happen to arrive within the window.
46
+ * - **TOCTOU on stale-lock unlink:** two processes both decide the lock
47
+ * is stale and both `unlinkSync`. Second `unlinkSync` ENOENT is
48
+ * swallowed. Both then race the `openSync("wx")` — one wins, the
49
+ * other goes back to polling. No data corruption.
50
+ * - **NFS / non-POSIX FS:** O_EXCL is undefined on stale NFSv2; modern
51
+ * NFS (≥ v3 with `noac`) and all local FS we ship on (apfs / ext4 /
52
+ * ntfs) implement it correctly.
53
+ *
54
+ * @param file - The target file the caller is about to write. The lock
55
+ * sentinel is `${file}.lock`.
56
+ * @param fn - Synchronous critical section. Holds the lock for its
57
+ * entire duration.
58
+ * @param opts.timeoutMs - Max wait for the lock before throwing (default
59
+ * 5000). Use a small value — appends finish in <1ms in practice; a
60
+ * timeout this far above the expected duration only fires if
61
+ * something genuinely wrong happens.
62
+ * @param opts.staleLockMs - Age (ms) at which an existing lock is
63
+ * considered abandoned and force-unlinked. Default 30000.
64
+ */
65
+ export function withFileLockSync(file, fn, opts = {}) {
66
+ const lockFile = `${file}.lock`;
67
+ const timeoutMs = opts.timeoutMs ?? 5000;
68
+ const staleLockMs = opts.staleLockMs ?? 30_000;
69
+ const deadline = Date.now() + timeoutMs;
70
+ let lockFd = null;
71
+ while (lockFd === null) {
72
+ try {
73
+ // O_EXCL — atomic create; first caller wins.
74
+ lockFd = openSync(lockFile, "wx", 0o600);
75
+ }
76
+ catch (err) {
77
+ const code = err.code;
78
+ if (code !== "EEXIST")
79
+ throw err;
80
+ // Stale-lock cleanup. Best-effort: another contender may unlink
81
+ // ours simultaneously; that race resolves benignly (next openSync
82
+ // either wins or sees EEXIST again).
83
+ try {
84
+ const stat = statSync(lockFile);
85
+ if (Date.now() - stat.mtimeMs > staleLockMs) {
86
+ try {
87
+ unlinkSync(lockFile);
88
+ }
89
+ catch {
90
+ /* already gone — another contender beat us to the cleanup */
91
+ }
92
+ continue;
93
+ }
94
+ }
95
+ catch {
96
+ // statSync ENOENT — lock cleared between EEXIST and stat; retry
97
+ // immediately.
98
+ continue;
99
+ }
100
+ if (Date.now() > deadline) {
101
+ throw new Error(`withFileLockSync: timed out after ${timeoutMs}ms waiting for ${lockFile}`);
102
+ }
103
+ // Busy-spin for ~5ms via hrtime. Node has no sync sleep; under
104
+ // contention the kernel preempts us, so this isn't actually a hot
105
+ // loop in practice (and tests can pass a 0ms timeout for the
106
+ // "already locked" case).
107
+ const spinUntil = process.hrtime.bigint() + 5000000n; // 5ms
108
+ while (process.hrtime.bigint() < spinUntil) {
109
+ /* yield */
110
+ }
111
+ }
112
+ }
113
+ try {
114
+ closeSync(lockFd);
115
+ return fn();
116
+ }
117
+ finally {
118
+ try {
119
+ unlinkSync(lockFile);
120
+ }
121
+ catch {
122
+ /* lock cleared by stale-lock cleanup in another process; ok */
123
+ }
124
+ }
125
+ }
126
+ //# sourceMappingURL=fileLockSync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileLockSync.js","sourceRoot":"","sources":["../src/fileLockSync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,EAAW,EACX,OAAqD,EAAE;IAEvD,MAAM,QAAQ,GAAG,GAAG,IAAI,OAAO,CAAC;IAChC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC;IACzC,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,MAAM,CAAC;IAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;IAExC,IAAI,MAAM,GAAkB,IAAI,CAAC;IACjC,OAAO,MAAM,KAAK,IAAI,EAAE,CAAC;QACvB,IAAI,CAAC;YACH,6CAA6C;YAC7C,MAAM,GAAG,QAAQ,CAAC,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QAC3C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,GAAI,GAA6B,CAAC,IAAI,CAAC;YACjD,IAAI,IAAI,KAAK,QAAQ;gBAAE,MAAM,GAAG,CAAC;YACjC,gEAAgE;YAChE,kEAAkE;YAClE,qCAAqC;YACrC,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;gBAChC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,OAAO,GAAG,WAAW,EAAE,CAAC;oBAC5C,IAAI,CAAC;wBACH,UAAU,CAAC,QAAQ,CAAC,CAAC;oBACvB,CAAC;oBAAC,MAAM,CAAC;wBACP,6DAA6D;oBAC/D,CAAC;oBACD,SAAS;gBACX,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,gEAAgE;gBAChE,eAAe;gBACf,SAAS;YACX,CAAC;YACD,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;gBAC1B,MAAM,IAAI,KAAK,CACb,qCAAqC,SAAS,kBAAkB,QAAQ,EAAE,CAC3E,CAAC;YACJ,CAAC;YACD,+DAA+D;YAC/D,kEAAkE;YAClE,6DAA6D;YAC7D,0BAA0B;YAC1B,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,QAAU,CAAC,CAAC,MAAM;YAC9D,OAAO,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,SAAS,EAAE,CAAC;gBAC3C,WAAW;YACb,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,CAAC;QACH,SAAS,CAAC,MAAM,CAAC,CAAC;QAClB,OAAO,EAAE,EAAE,CAAC;IACd,CAAC;YAAS,CAAC;QACT,IAAI,CAAC;YACH,UAAU,CAAC,QAAQ,CAAC,CAAC;QACvB,CAAC;QAAC,MAAM,CAAC;YACP,+DAA+D;QACjE,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -10,6 +10,12 @@ import { type ToolResult } from "./result.js";
10
10
  /**
11
11
  * Returns true if `value` matches the condition pattern.
12
12
  * Negation via "!" prefix. Missing pattern → always true.
13
+ *
14
+ * On Windows, file paths arrive as `C:\Users\foo\src\bar.ts` while users
15
+ * write POSIX-style globs (`src/**\/*.ts`). minimatch is strict on `/` and
16
+ * NTFS is case-insensitive, so without normalisation onFileSave/onFileChanged
17
+ * hooks silently never fire on Windows. Normalise backslashes to forward
18
+ * slashes on both sides and pass `nocase` on win32.
13
19
  */
14
20
  export declare function matchesCondition(pattern: string | undefined, value: string): boolean;
15
21
  /**
@@ -16,15 +16,28 @@ function emptyAcc(state) {
16
16
  /**
17
17
  * Returns true if `value` matches the condition pattern.
18
18
  * Negation via "!" prefix. Missing pattern → always true.
19
+ *
20
+ * On Windows, file paths arrive as `C:\Users\foo\src\bar.ts` while users
21
+ * write POSIX-style globs (`src/**\/*.ts`). minimatch is strict on `/` and
22
+ * NTFS is case-insensitive, so without normalisation onFileSave/onFileChanged
23
+ * hooks silently never fire on Windows. Normalise backslashes to forward
24
+ * slashes on both sides and pass `nocase` on win32.
19
25
  */
20
26
  export function matchesCondition(pattern, value) {
21
27
  if (pattern === undefined || pattern === "")
22
28
  return true;
29
+ const isWin = process.platform === "win32";
30
+ const normValue = isWin ? value.replace(/\\/g, "/") : value;
31
+ const opts = isWin ? { dot: true, nocase: true } : { dot: true };
23
32
  try {
24
33
  if (pattern.startsWith("!")) {
25
- return !minimatch(value, pattern.slice(1), { dot: true });
34
+ const inner = isWin
35
+ ? pattern.slice(1).replace(/\\/g, "/")
36
+ : pattern.slice(1);
37
+ return !minimatch(normValue, inner, opts);
26
38
  }
27
- return minimatch(value, pattern, { dot: true });
39
+ const normPattern = isWin ? pattern.replace(/\\/g, "/") : pattern;
40
+ return minimatch(normValue, normPattern, opts);
28
41
  }
29
42
  catch {
30
43
  return false;