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
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Idempotency keys for write-tool calls.
3
+ *
4
+ * PR5a of the Val-inspired plan. Foundation for safe retry + safe resume.
5
+ *
6
+ * Two pieces:
7
+ *
8
+ * `deriveIdempotencyKey(toolId, params)`
9
+ * A stable, deterministic hash over `(toolId, canonicalised params)`.
10
+ * Canonicalisation = JSON.stringify with sorted keys, recursive — so
11
+ * `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` hash identically. Returns a
12
+ * hex SHA-256 prefix (first 16 chars; collisions vanishingly small
13
+ * within a single run scope).
14
+ *
15
+ * `WriteEffectLedger`
16
+ * Per-run in-memory map of key → cached output. The runner constructs
17
+ * one per recipe run and threads it through `StepDeps` / `ToolContext`.
18
+ * `toolRegistry.executeTool` checks the ledger before invoking write
19
+ * tools; if the key is present, returns the cached output instead of
20
+ * re-executing — preventing duplicate side effects when two parallel
21
+ * branches of a chained recipe both call the same write tool with the
22
+ * same params.
23
+ *
24
+ * Scope of this PR (deliberately narrow):
25
+ * - In-run dedup only (Map lives for one recipe run, discarded after).
26
+ * - Records only on successful execution; errors don't pollute the
27
+ * ledger, so retry-after-failure still re-executes (correct: if the
28
+ * tool errored, we can't assume the side effect happened).
29
+ * - No cross-run persistence — that's PR5b (disk-backed effect ledger).
30
+ * - No retry-time idempotency on partial-failure cases (Slack posted
31
+ * but HTTP timed out); that needs tool-side support and is a future
32
+ * PR.
33
+ *
34
+ * The protection this DOES provide today: a `parallel:` block (or a
35
+ * recipe that calls a write tool from two different chained steps with
36
+ * identical params) cannot duplicate the side effect. Concretely, this
37
+ * was a footgun that pre-dated PR5a: `chainedRunner.ts` schedules steps
38
+ * with dependency-graph parallelism; if two branches happen to call
39
+ * `slack.postMessage` with the same payload, the message went twice.
40
+ */
41
+ import type { Logger } from "../logger.js";
42
+ /**
43
+ * Derive a stable idempotency key for a write-tool invocation. 16 hex
44
+ * chars is 64 bits of entropy — far more than enough for in-run dedup
45
+ * (a single recipe with even 10⁵ steps has ~5×10⁻¹⁰ collision risk).
46
+ */
47
+ export declare function deriveIdempotencyKey(toolId: string, params: Record<string, unknown>): string;
48
+ /**
49
+ * Compose a collision-safe scope key from `(recipeName, manualRunId)`.
50
+ *
51
+ * Naive `${recipeName}:${manualRunId}` is ambiguous: recipe `a:b` +
52
+ * attempt `c` and recipe `a` + attempt `b:c` both produce `a:b:c` and
53
+ * would share a ledger scope, letting one attempt read another's
54
+ * cached write-tool outputs. We hash both fields separately as a JSON
55
+ * array so the encoding is unambiguous regardless of either field's
56
+ * contents.
57
+ *
58
+ * Returned as a 32-hex-char SHA-256 prefix — long enough that
59
+ * collisions across a realistic ledger are effectively impossible
60
+ * (~2^128 birthday bound), short enough to scan in a JSONL row.
61
+ */
62
+ export declare function deriveScopeKey(recipeName: string, manualRunId: string): string;
63
+ export declare function assertValidManualRunId(id: string): string;
64
+ /**
65
+ * In-memory per-run ledger of executed write-tool calls. Maps idempotency
66
+ * keys to the cached output the tool returned, so a duplicate call can
67
+ * be short-circuited to the same result the first call produced.
68
+ *
69
+ * The ledger is single-threaded by design — runners are single-process
70
+ * and a per-run ledger has no cross-thread access. Concurrency safety
71
+ * within a run is provided by the dependency graph (parallel-only steps
72
+ * with no shared params hash by construction); the ledger catches
73
+ * accidental same-params calls.
74
+ */
75
+ /**
76
+ * Optional disk-backed persistence for the ledger.
77
+ *
78
+ * PR5b — extends in-memory dedup so a *retry* of the same logical
79
+ * `(recipeName, manualRunId)` attempt won't replay side effects. The
80
+ * ledger stays per-attempt; cron/webhook runs and recipes without a
81
+ * manualRunId stay purely in memory (no scope key = nothing to write).
82
+ *
83
+ * File layout: a single JSONL at `${dir}/effect_ledger.jsonl`. Each row
84
+ * is `{scopeKey, idemKey, output, recordedAt}`. On construction, the
85
+ * ledger streams the file and rehydrates entries whose `scopeKey`
86
+ * matches the configured scope; everything else is left alone for the
87
+ * other attempts' ledgers to pick up.
88
+ *
89
+ * Failure mode: any IO error falls back to in-memory operation and logs
90
+ * a warning. A partially-replayed attempt with an unreadable ledger
91
+ * degrades to "re-execute side effects" — louder than "silently dedup
92
+ * something we can't audit".
93
+ */
94
+ export interface DiskLedgerOptions {
95
+ /** Directory holding `effect_ledger.jsonl`. Created if missing. */
96
+ dir: string;
97
+ /** `${recipeName}:${manualRunId}` — composed by the caller. */
98
+ scopeKey: string;
99
+ logger?: Logger;
100
+ }
101
+ export declare class WriteEffectLedger {
102
+ private readonly cache;
103
+ private readonly disk;
104
+ private readonly file;
105
+ constructor(disk?: DiskLedgerOptions);
106
+ has(key: string): boolean;
107
+ /**
108
+ * Return the previously-cached output for `key`, or `undefined` if not
109
+ * recorded. `null` is a legitimate cached value (= the tool returned
110
+ * `null` originally), so callers must use `has()` to distinguish "not
111
+ * present" from "present and null".
112
+ */
113
+ get(key: string): string | null | undefined;
114
+ record(key: string, output: string | null): void;
115
+ /** Test-only inspection of the current key set. */
116
+ keys(): string[];
117
+ size(): number;
118
+ private loadExisting;
119
+ private append;
120
+ /**
121
+ * Trim `effect_ledger.jsonl` to the most recent MAX_PERSIST_LINES.
122
+ * Best-effort — failure logs and the next append proceeds against the
123
+ * un-rotated file. Same pattern as RecipeRunLog / DecisionTraceLog.
124
+ */
125
+ private rotate;
126
+ }
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Idempotency keys for write-tool calls.
3
+ *
4
+ * PR5a of the Val-inspired plan. Foundation for safe retry + safe resume.
5
+ *
6
+ * Two pieces:
7
+ *
8
+ * `deriveIdempotencyKey(toolId, params)`
9
+ * A stable, deterministic hash over `(toolId, canonicalised params)`.
10
+ * Canonicalisation = JSON.stringify with sorted keys, recursive — so
11
+ * `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` hash identically. Returns a
12
+ * hex SHA-256 prefix (first 16 chars; collisions vanishingly small
13
+ * within a single run scope).
14
+ *
15
+ * `WriteEffectLedger`
16
+ * Per-run in-memory map of key → cached output. The runner constructs
17
+ * one per recipe run and threads it through `StepDeps` / `ToolContext`.
18
+ * `toolRegistry.executeTool` checks the ledger before invoking write
19
+ * tools; if the key is present, returns the cached output instead of
20
+ * re-executing — preventing duplicate side effects when two parallel
21
+ * branches of a chained recipe both call the same write tool with the
22
+ * same params.
23
+ *
24
+ * Scope of this PR (deliberately narrow):
25
+ * - In-run dedup only (Map lives for one recipe run, discarded after).
26
+ * - Records only on successful execution; errors don't pollute the
27
+ * ledger, so retry-after-failure still re-executes (correct: if the
28
+ * tool errored, we can't assume the side effect happened).
29
+ * - No cross-run persistence — that's PR5b (disk-backed effect ledger).
30
+ * - No retry-time idempotency on partial-failure cases (Slack posted
31
+ * but HTTP timed out); that needs tool-side support and is a future
32
+ * PR.
33
+ *
34
+ * The protection this DOES provide today: a `parallel:` block (or a
35
+ * recipe that calls a write tool from two different chained steps with
36
+ * identical params) cannot duplicate the side effect. Concretely, this
37
+ * was a footgun that pre-dated PR5a: `chainedRunner.ts` schedules steps
38
+ * with dependency-graph parallelism; if two branches happen to call
39
+ * `slack.postMessage` with the same payload, the message went twice.
40
+ */
41
+ import { createHash } from "node:crypto";
42
+ import { appendFileSync, lstatSync, mkdirSync, readFileSync, statSync, } from "node:fs";
43
+ import path from "node:path";
44
+ import { writeFileAtomicSync } from "../writeFileAtomic.js";
45
+ /**
46
+ * Stable canonical-JSON serialiser. Recursively sorts object keys so two
47
+ * params records with the same shape but different key order produce the
48
+ * same string. Plain objects only — falls back to `JSON.stringify` for
49
+ * arrays / primitives / null.
50
+ */
51
+ function canonicalise(value) {
52
+ if (value === null || typeof value !== "object") {
53
+ return JSON.stringify(value);
54
+ }
55
+ if (Array.isArray(value)) {
56
+ return `[${value.map(canonicalise).join(",")}]`;
57
+ }
58
+ const entries = Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
59
+ const body = entries
60
+ .map(([k, v]) => `${JSON.stringify(k)}:${canonicalise(v)}`)
61
+ .join(",");
62
+ return `{${body}}`;
63
+ }
64
+ /**
65
+ * Derive a stable idempotency key for a write-tool invocation. 16 hex
66
+ * chars is 64 bits of entropy — far more than enough for in-run dedup
67
+ * (a single recipe with even 10⁵ steps has ~5×10⁻¹⁰ collision risk).
68
+ */
69
+ export function deriveIdempotencyKey(toolId, params) {
70
+ const payload = `${toolId}|${canonicalise(params)}`;
71
+ return createHash("sha256").update(payload).digest("hex").slice(0, 16);
72
+ }
73
+ /**
74
+ * Compose a collision-safe scope key from `(recipeName, manualRunId)`.
75
+ *
76
+ * Naive `${recipeName}:${manualRunId}` is ambiguous: recipe `a:b` +
77
+ * attempt `c` and recipe `a` + attempt `b:c` both produce `a:b:c` and
78
+ * would share a ledger scope, letting one attempt read another's
79
+ * cached write-tool outputs. We hash both fields separately as a JSON
80
+ * array so the encoding is unambiguous regardless of either field's
81
+ * contents.
82
+ *
83
+ * Returned as a 32-hex-char SHA-256 prefix — long enough that
84
+ * collisions across a realistic ledger are effectively impossible
85
+ * (~2^128 birthday bound), short enough to scan in a JSONL row.
86
+ */
87
+ export function deriveScopeKey(recipeName, manualRunId) {
88
+ const payload = JSON.stringify([recipeName, manualRunId]);
89
+ return createHash("sha256").update(payload).digest("hex").slice(0, 32);
90
+ }
91
+ /**
92
+ * `manualRunId` charset validation — caller-supplied id from the CLI
93
+ * (`--attempt`), HTTP routes, or SDK. Rejects null bytes, control
94
+ * characters, path-traversal slugs (`/`, `\`, `..`), and anything
95
+ * longer than 64 chars. Returns the id verbatim when valid; throws
96
+ * with a descriptive message otherwise.
97
+ *
98
+ * Why strict: this string is hashed into the disk-ledger scope key,
99
+ * appended to `runs.jsonl` rows (capped audit-row size depends on it),
100
+ * and rendered into dashboard pills + CLI output. A 10 MB id would
101
+ * inflate every row past `MAX_PERSIST_BYTES` and erase audit during
102
+ * rotation; control characters break line-delimited persistence.
103
+ */
104
+ const MANUAL_RUN_ID_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
105
+ export function assertValidManualRunId(id) {
106
+ if (typeof id !== "string" || !MANUAL_RUN_ID_PATTERN.test(id)) {
107
+ throw new Error(`manualRunId must match ${MANUAL_RUN_ID_PATTERN} (1-64 chars of [A-Za-z0-9_.-]); got: ${JSON.stringify(id).slice(0, 80)}`);
108
+ }
109
+ return id;
110
+ }
111
+ const LEDGER_FILENAME = "effect_ledger.jsonl";
112
+ const MAX_PERSIST_BYTES = 1024 * 1024; // 1 MB — same posture as runLog
113
+ const MAX_PERSIST_LINES = 10_000;
114
+ /**
115
+ * Validate a caller-supplied ledger directory before any filesystem IO.
116
+ *
117
+ * The dir argument flows from the CLI (`--ledger-dir`) and (if/when
118
+ * exposed) HTTP-runner inputs; we'd rather fail loudly than write JSONL
119
+ * to whatever path is handed in. Three checks:
120
+ *
121
+ * - **No null bytes** — would short-circuit C string handling in older
122
+ * libc paths and confuse logs / audit tooling.
123
+ * - **Absolute** — relative paths resolve against `process.cwd()`,
124
+ * which for recipe runs is the workspace; an `--ledger-dir foo`
125
+ * silently scattering ledger files under recipe sources is the
126
+ * wrong default. Caller can pass `path.resolve(...)` explicitly if
127
+ * they want relative resolution.
128
+ * - **Not a symlink** — if the directory already exists and is a
129
+ * symlink, an attacker who can write to the symlink's owning dir
130
+ * can swap the target and redirect appends. Rejecting up front
131
+ * means a fresh dir is created (via mkdirSync) or an existing real
132
+ * dir is used; symlink-replacement on the JSONL file itself is
133
+ * handled separately on each read in `loadExisting`.
134
+ */
135
+ function assertSafeLedgerDir(dir) {
136
+ if (typeof dir !== "string" || dir.length === 0) {
137
+ throw new Error("ledgerDir must be a non-empty string");
138
+ }
139
+ if (dir.includes("\0")) {
140
+ throw new Error("ledgerDir must not contain null bytes");
141
+ }
142
+ if (!path.isAbsolute(dir)) {
143
+ throw new Error(`ledgerDir must be an absolute path; got: ${dir}`);
144
+ }
145
+ try {
146
+ const st = lstatSync(dir);
147
+ if (st.isSymbolicLink()) {
148
+ throw new Error(`ledgerDir must not be a symlink: ${dir}`);
149
+ }
150
+ }
151
+ catch (err) {
152
+ const code = err.code;
153
+ if (code === "ENOENT")
154
+ return; // dir will be created later — fine
155
+ throw err;
156
+ }
157
+ }
158
+ export class WriteEffectLedger {
159
+ cache = new Map();
160
+ disk;
161
+ file;
162
+ constructor(disk) {
163
+ if (disk) {
164
+ // Validate + normalise the directory before any IO. Rejects null
165
+ // bytes (would short-circuit C string handling in libc paths),
166
+ // requires absolute paths (relative paths resolve against cwd
167
+ // which is the recipe workspace — surprising and racy), and
168
+ // refuses symlinks (a symlink swap on the ledger directory after
169
+ // construction could redirect appends to an attacker-chosen
170
+ // path).
171
+ assertSafeLedgerDir(disk.dir);
172
+ }
173
+ this.disk = disk ?? null;
174
+ this.file = disk ? path.join(disk.dir, LEDGER_FILENAME) : null;
175
+ if (this.disk && this.file) {
176
+ try {
177
+ mkdirSync(this.disk.dir, { recursive: true, mode: 0o700 });
178
+ }
179
+ catch (err) {
180
+ this.disk.logger?.warn?.(`[effect-ledger] could not create ${this.disk.dir}: ${err instanceof Error ? err.message : String(err)}`);
181
+ }
182
+ this.loadExisting();
183
+ }
184
+ }
185
+ has(key) {
186
+ return this.cache.has(key);
187
+ }
188
+ /**
189
+ * Return the previously-cached output for `key`, or `undefined` if not
190
+ * recorded. `null` is a legitimate cached value (= the tool returned
191
+ * `null` originally), so callers must use `has()` to distinguish "not
192
+ * present" from "present and null".
193
+ */
194
+ get(key) {
195
+ return this.cache.get(key);
196
+ }
197
+ record(key, output) {
198
+ this.cache.set(key, output);
199
+ if (this.disk && this.file) {
200
+ this.append({
201
+ scopeKey: this.disk.scopeKey,
202
+ idemKey: key,
203
+ output,
204
+ recordedAt: Date.now(),
205
+ });
206
+ }
207
+ }
208
+ /** Test-only inspection of the current key set. */
209
+ keys() {
210
+ return Array.from(this.cache.keys());
211
+ }
212
+ size() {
213
+ return this.cache.size;
214
+ }
215
+ loadExisting() {
216
+ if (!this.disk || !this.file)
217
+ return;
218
+ let raw;
219
+ try {
220
+ // `lstat` (not `stat`) so we see the symlink, not its target.
221
+ // A swapped symlink at `${dir}/effect_ledger.jsonl` would
222
+ // otherwise let an attacker substitute another file's contents
223
+ // as cached tool outputs.
224
+ const st = lstatSync(this.file);
225
+ if (st.isSymbolicLink()) {
226
+ this.disk.logger?.warn?.(`[effect-ledger] refusing to load ${this.file}: file is a symlink`);
227
+ return;
228
+ }
229
+ raw = readFileSync(this.file, "utf-8");
230
+ }
231
+ catch (err) {
232
+ const code = err.code;
233
+ if (code !== "ENOENT") {
234
+ this.disk.logger?.warn?.(`[effect-ledger] read failed: ${err instanceof Error ? err.message : String(err)}`);
235
+ }
236
+ return;
237
+ }
238
+ for (const line of raw.split("\n")) {
239
+ if (!line)
240
+ continue;
241
+ try {
242
+ const row = JSON.parse(line);
243
+ if (typeof row.scopeKey !== "string" ||
244
+ typeof row.idemKey !== "string") {
245
+ continue;
246
+ }
247
+ if (row.scopeKey === this.disk.scopeKey) {
248
+ this.cache.set(row.idemKey, row.output ?? null);
249
+ }
250
+ }
251
+ catch {
252
+ /* skip malformed row */
253
+ }
254
+ }
255
+ }
256
+ append(row) {
257
+ if (!this.disk || !this.file)
258
+ return;
259
+ try {
260
+ try {
261
+ const st = statSync(this.file);
262
+ if (st.size > MAX_PERSIST_BYTES)
263
+ this.rotate();
264
+ }
265
+ catch (err) {
266
+ const code = err.code;
267
+ if (code !== "ENOENT")
268
+ throw err;
269
+ }
270
+ appendFileSync(this.file, `${JSON.stringify(row)}\n`, { mode: 0o600 });
271
+ }
272
+ catch (err) {
273
+ this.disk.logger?.warn?.(`[effect-ledger] append failed: ${err instanceof Error ? err.message : String(err)}`);
274
+ }
275
+ }
276
+ /**
277
+ * Trim `effect_ledger.jsonl` to the most recent MAX_PERSIST_LINES.
278
+ * Best-effort — failure logs and the next append proceeds against the
279
+ * un-rotated file. Same pattern as RecipeRunLog / DecisionTraceLog.
280
+ */
281
+ rotate() {
282
+ if (!this.file || !this.disk)
283
+ return;
284
+ try {
285
+ const raw = readFileSync(this.file, "utf-8");
286
+ let lines = raw.split("\n").filter((l) => l.trim());
287
+ if (lines.length > MAX_PERSIST_LINES) {
288
+ lines = lines.slice(-MAX_PERSIST_LINES);
289
+ }
290
+ writeFileAtomicSync(this.file, lines.length > 0 ? `${lines.join("\n")}\n` : "", { mode: 0o600 });
291
+ }
292
+ catch (err) {
293
+ this.disk.logger?.warn?.(`[effect-ledger] rotate failed: ${err instanceof Error ? err.message : String(err)}`);
294
+ }
295
+ }
296
+ }
297
+ //# sourceMappingURL=idempotencyKey.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotencyKey.js","sourceRoot":"","sources":["../../src/recipes/idempotencyKey.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,40 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import crypto from "node:crypto";
2
+ import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
2
3
  import path from "node:path";
3
4
  import { parse as parseYaml } from "yaml";
4
5
  import { compileRecipeFull } from "./compiler.js";
5
6
  import { parseRecipe } from "./parser.js";
7
+ /**
8
+ * Atomic temp+rename for the default install write. Audit 2026-05-17:
9
+ * two concurrent installs of the same recipe (cross-process, e.g.
10
+ * dashboard + CLI racing) used to interleave bytes within the JSON
11
+ * payload because the previous `writeFileSync(destPath, ...)` is not
12
+ * atomic for sub-page writes. A torn JSON file fails to parse and the
13
+ * recipe becomes invisible to the scheduler.
14
+ *
15
+ * `rename` is atomic at the FS layer on every platform we ship on
16
+ * (apfs / ext4 / ntfs / xfs). With temp+rename, two concurrent writers
17
+ * each end up with their own intact file → last `rename` wins, but
18
+ * the file on disk is ALWAYS a valid JSON document.
19
+ */
20
+ function atomicWriteSync(target, content) {
21
+ const tmp = `${target}.tmp.${process.pid}.${crypto
22
+ .randomBytes(6)
23
+ .toString("hex")}`;
24
+ try {
25
+ writeFileSync(tmp, content);
26
+ renameSync(tmp, target);
27
+ }
28
+ catch (err) {
29
+ try {
30
+ unlinkSync(tmp);
31
+ }
32
+ catch {
33
+ /* already gone or never created */
34
+ }
35
+ throw err;
36
+ }
37
+ }
6
38
  export function installRecipeFromFile(sourcePath, opts) {
7
39
  const ext = path.extname(sourcePath).toLowerCase();
8
40
  if (ext !== ".json" && ext !== ".yaml" && ext !== ".yml") {
@@ -10,7 +42,10 @@ export function installRecipeFromFile(sourcePath, opts) {
10
42
  }
11
43
  const fs = opts.fs ?? {};
12
44
  const readFile = fs.readFile ?? ((p) => readFileSync(p, "utf-8"));
13
- const writeFile = fs.writeFile ?? ((p, c) => writeFileSync(p, c));
45
+ // Default writer is atomic (temp+rename). Callers may inject their
46
+ // own writeFile (tests, in-memory fs); they are responsible for
47
+ // their own atomicity guarantees.
48
+ const writeFile = fs.writeFile ?? atomicWriteSync;
14
49
  const mkdir = fs.mkdir ?? ((p) => mkdirSync(p, { recursive: true }));
15
50
  const text = readFile(sourcePath);
16
51
  const raw = ext === ".json"
@@ -39,6 +74,17 @@ export function installRecipeFromFile(sourcePath, opts) {
39
74
  : compileRecipeFull(recipe);
40
75
  mkdir(opts.recipesDir);
41
76
  const destPath = path.join(opts.recipesDir, `${recipe.name}.json`);
77
+ // Belt-and-braces: parseRecipe already constrains `name` to the
78
+ // RECIPE_NAME_RE charset, but assert the resolved path stays inside
79
+ // recipesDir before writing. Defends against any future parser bypass
80
+ // and against callers reaching this function with a pre-parsed object
81
+ // that skipped parseRecipe.
82
+ const resolvedDir = path.resolve(opts.recipesDir);
83
+ const resolvedDest = path.resolve(destPath);
84
+ if (resolvedDest !== resolvedDir &&
85
+ !resolvedDest.startsWith(resolvedDir + path.sep)) {
86
+ throw new Error(`installRecipeFromFile: refusing to write outside recipesDir (dest=${resolvedDest} dir=${resolvedDir})`);
87
+ }
42
88
  let action = "created";
43
89
  try {
44
90
  readFile(destPath);
@@ -1 +1 @@
1
- {"version":3,"file":"installer.js","sourceRoot":"","sources":["../../src/recipes/installer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"installer.js","sourceRoot":"","sources":["../../src/recipes/installer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Judge-verdict aggregation — PR3b.
3
+ *
4
+ * Parallel to haltCategory.summariseHalts: walks a window of runs and
5
+ * counts judge-step verdicts (`approve` / `request_changes` /
6
+ * `unparseable`). Surfaces through:
7
+ *
8
+ * - `/metrics` as `bridge_recipe_judgments{verdict="..."}` gauge
9
+ * - (later) dashboard panel + session-start digest in PR3c
10
+ *
11
+ * Augment-only invariant (see judgeVerdict.ts): a `request_changes`
12
+ * verdict never appears as a HaltCategory and never causes
13
+ * `status: "error"`. This module is the *separate* channel that makes
14
+ * cold-eyes review visible without re-introducing gate semantics.
15
+ */
16
+ import type { JudgeVerdict, JudgeVerdictKind } from "./judgeVerdict.js";
17
+ export interface JudgeSummary {
18
+ /** Total step results scanned that carry a `judgeVerdict`. */
19
+ total: number;
20
+ /** Per-verdict counts; verdicts with zero hits are omitted. */
21
+ byVerdict: Partial<Record<JudgeVerdictKind, number>>;
22
+ /** Most recent 5 verdicts (with first reason) for UI surfacing. */
23
+ recent: Array<{
24
+ verdict: JudgeVerdictKind;
25
+ firstReason?: string;
26
+ runSeq: number;
27
+ stepId: string;
28
+ }>;
29
+ }
30
+ interface JudgeSummaryInputRun {
31
+ seq: number;
32
+ stepResults?: Array<{
33
+ id: string;
34
+ judgeVerdict?: JudgeVerdict;
35
+ }>;
36
+ }
37
+ /**
38
+ * Aggregate judge verdicts across a set of runs. Runs are expected to
39
+ * be sorted newest-first so `recent` reflects the most recent
40
+ * verdicts.
41
+ */
42
+ export declare function summariseJudgments(runs: JudgeSummaryInputRun[]): JudgeSummary;
43
+ /**
44
+ * Format a `JudgeSummary` as Prometheus text-exposition lines for the
45
+ * `bridge_recipe_judgments{verdict="..."} N` gauge. Returns an empty
46
+ * array when the summary is empty (no HELP/TYPE block so Prom scrapers
47
+ * don't see an orphan declaration).
48
+ */
49
+ export declare function judgeSummaryToPrometheus(summary: JudgeSummary): string[];
50
+ export {};
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Aggregate judge verdicts across a set of runs. Runs are expected to
3
+ * be sorted newest-first so `recent` reflects the most recent
4
+ * verdicts.
5
+ */
6
+ export function summariseJudgments(runs) {
7
+ const byVerdict = {};
8
+ const recent = [];
9
+ let total = 0;
10
+ for (const run of runs) {
11
+ for (const step of run.stepResults ?? []) {
12
+ const v = step.judgeVerdict;
13
+ if (!v)
14
+ continue;
15
+ total++;
16
+ byVerdict[v.verdict] = (byVerdict[v.verdict] ?? 0) + 1;
17
+ if (recent.length < 5) {
18
+ recent.push({
19
+ verdict: v.verdict,
20
+ ...(v.reasons[0] !== undefined && { firstReason: v.reasons[0] }),
21
+ runSeq: run.seq,
22
+ stepId: step.id,
23
+ });
24
+ }
25
+ }
26
+ }
27
+ return { total, byVerdict, recent };
28
+ }
29
+ /**
30
+ * Format a `JudgeSummary` as Prometheus text-exposition lines for the
31
+ * `bridge_recipe_judgments{verdict="..."} N` gauge. Returns an empty
32
+ * array when the summary is empty (no HELP/TYPE block so Prom scrapers
33
+ * don't see an orphan declaration).
34
+ */
35
+ export function judgeSummaryToPrometheus(summary) {
36
+ if (summary.total === 0)
37
+ return [];
38
+ const lines = [
39
+ "# HELP bridge_recipe_judgments Recipe judge-step verdicts in the in-memory run-log window, by verdict",
40
+ "# TYPE bridge_recipe_judgments gauge",
41
+ ];
42
+ for (const [verdict, count] of Object.entries(summary.byVerdict)) {
43
+ lines.push(`bridge_recipe_judgments{verdict="${verdict}"} ${count}`);
44
+ }
45
+ return lines;
46
+ }
47
+ //# sourceMappingURL=judgeSummary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"judgeSummary.js","sourceRoot":"","sources":["../../src/recipes/judgeSummary.ts"],"names":[],"mappings":"AAuCA;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAA4B;IAC7D,MAAM,SAAS,GAA8C,EAAE,CAAC;IAChE,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,WAAW,IAAI,EAAE,EAAE,CAAC;YACzC,MAAM,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC;YAC5B,IAAI,CAAC,CAAC;gBAAE,SAAS;YACjB,KAAK,EAAE,CAAC;YACR,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;YACvD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtB,MAAM,CAAC,IAAI,CAAC;oBACV,OAAO,EAAE,CAAC,CAAC,OAAO;oBAClB,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;oBAChE,MAAM,EAAE,GAAG,CAAC,GAAG;oBACf,MAAM,EAAE,IAAI,CAAC,EAAE;iBAChB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;AACtC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAqB;IAC5D,IAAI,OAAO,CAAC,KAAK,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACnC,MAAM,KAAK,GAAa;QACtB,uGAAuG;QACvG,sCAAsC;KACvC,CAAC;IACF,KAAK,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACjE,KAAK,CAAC,IAAI,CAAC,oCAAoC,OAAO,MAAM,KAAK,EAAE,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}