patchwork-os 0.2.0-alpha.34 → 0.2.0-alpha.36

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 (270) hide show
  1. package/README.md +202 -93
  2. package/deploy/bootstrap-new-vps.sh +12 -12
  3. package/deploy/bootstrap-vps.sh +6 -3
  4. package/deploy/deploy-landing.sh +59 -2
  5. package/dist/activityLog.d.ts +49 -0
  6. package/dist/activityLog.js +78 -0
  7. package/dist/activityLog.js.map +1 -1
  8. package/dist/approvalHttp.d.ts +25 -0
  9. package/dist/approvalHttp.js +74 -18
  10. package/dist/approvalHttp.js.map +1 -1
  11. package/dist/approvalInsights.d.ts +49 -0
  12. package/dist/approvalInsights.js +97 -0
  13. package/dist/approvalInsights.js.map +1 -0
  14. package/dist/approvalQueue.d.ts +11 -0
  15. package/dist/approvalQueue.js +80 -1
  16. package/dist/approvalQueue.js.map +1 -1
  17. package/dist/approvalSignals.d.ts +124 -0
  18. package/dist/approvalSignals.js +512 -0
  19. package/dist/approvalSignals.js.map +1 -0
  20. package/dist/automation.d.ts +37 -0
  21. package/dist/automation.js +105 -61
  22. package/dist/automation.js.map +1 -1
  23. package/dist/automationSuggestions.d.ts +79 -0
  24. package/dist/automationSuggestions.js +150 -0
  25. package/dist/automationSuggestions.js.map +1 -0
  26. package/dist/bridge.js +78 -1
  27. package/dist/bridge.js.map +1 -1
  28. package/dist/ccPermissions.d.ts +15 -0
  29. package/dist/ccPermissions.js +15 -0
  30. package/dist/ccPermissions.js.map +1 -1
  31. package/dist/claudeDriver.js +74 -16
  32. package/dist/claudeDriver.js.map +1 -1
  33. package/dist/commands/patchworkInit.d.ts +8 -0
  34. package/dist/commands/patchworkInit.js +41 -5
  35. package/dist/commands/patchworkInit.js.map +1 -1
  36. package/dist/commands/recipe.d.ts +20 -0
  37. package/dist/commands/recipe.js +212 -6
  38. package/dist/commands/recipe.js.map +1 -1
  39. package/dist/commands/recipeInstall.d.ts +79 -1
  40. package/dist/commands/recipeInstall.js +333 -16
  41. package/dist/commands/recipeInstall.js.map +1 -1
  42. package/dist/commands/tracesExport.d.ts +83 -0
  43. package/dist/commands/tracesExport.js +269 -0
  44. package/dist/commands/tracesExport.js.map +1 -0
  45. package/dist/commands/tracesImport.d.ts +56 -0
  46. package/dist/commands/tracesImport.js +161 -0
  47. package/dist/commands/tracesImport.js.map +1 -0
  48. package/dist/config.d.ts +8 -0
  49. package/dist/config.js +9 -1
  50. package/dist/config.js.map +1 -1
  51. package/dist/connectorRoutes.d.ts +43 -0
  52. package/dist/connectorRoutes.js +1023 -0
  53. package/dist/connectorRoutes.js.map +1 -0
  54. package/dist/connectors/asana.d.ts +198 -0
  55. package/dist/connectors/asana.js +679 -0
  56. package/dist/connectors/asana.js.map +1 -0
  57. package/dist/connectors/baseConnector.d.ts +36 -0
  58. package/dist/connectors/baseConnector.js +151 -28
  59. package/dist/connectors/baseConnector.js.map +1 -1
  60. package/dist/connectors/discord.d.ts +150 -0
  61. package/dist/connectors/discord.js +543 -0
  62. package/dist/connectors/discord.js.map +1 -0
  63. package/dist/connectors/github.js +11 -4
  64. package/dist/connectors/github.js.map +1 -1
  65. package/dist/connectors/gitlab.d.ts +180 -0
  66. package/dist/connectors/gitlab.js +582 -0
  67. package/dist/connectors/gitlab.js.map +1 -0
  68. package/dist/connectors/gmail.js +50 -10
  69. package/dist/connectors/gmail.js.map +1 -1
  70. package/dist/connectors/googleCalendar.js +36 -10
  71. package/dist/connectors/googleCalendar.js.map +1 -1
  72. package/dist/connectors/googleDrive.d.ts +34 -0
  73. package/dist/connectors/googleDrive.js +321 -0
  74. package/dist/connectors/googleDrive.js.map +1 -0
  75. package/dist/connectors/linear.js +23 -4
  76. package/dist/connectors/linear.js.map +1 -1
  77. package/dist/connectors/mcpOAuth.js +26 -2
  78. package/dist/connectors/mcpOAuth.js.map +1 -1
  79. package/dist/connectors/oauthStateStore.d.ts +31 -0
  80. package/dist/connectors/oauthStateStore.js +52 -0
  81. package/dist/connectors/oauthStateStore.js.map +1 -0
  82. package/dist/connectors/pagerduty.d.ts +160 -0
  83. package/dist/connectors/pagerduty.js +464 -0
  84. package/dist/connectors/pagerduty.js.map +1 -0
  85. package/dist/connectors/slack.d.ts +16 -1
  86. package/dist/connectors/slack.js +57 -5
  87. package/dist/connectors/slack.js.map +1 -1
  88. package/dist/connectors/tokenStorage.js +27 -2
  89. package/dist/connectors/tokenStorage.js.map +1 -1
  90. package/dist/connectors/zendesk.js +19 -1
  91. package/dist/connectors/zendesk.js.map +1 -1
  92. package/dist/cors.d.ts +10 -0
  93. package/dist/cors.js +29 -0
  94. package/dist/cors.js.map +1 -0
  95. package/dist/decisionReplay.d.ts +72 -0
  96. package/dist/decisionReplay.js +92 -0
  97. package/dist/decisionReplay.js.map +1 -0
  98. package/dist/decisionTraceLog.d.ts +6 -0
  99. package/dist/decisionTraceLog.js +54 -2
  100. package/dist/decisionTraceLog.js.map +1 -1
  101. package/dist/featureFlags.d.ts +17 -11
  102. package/dist/featureFlags.js +52 -47
  103. package/dist/featureFlags.js.map +1 -1
  104. package/dist/fp/automationInterpreter.js +25 -21
  105. package/dist/fp/automationInterpreter.js.map +1 -1
  106. package/dist/fp/automationState.js +4 -1
  107. package/dist/fp/automationState.js.map +1 -1
  108. package/dist/fp/policyParser.js +4 -1
  109. package/dist/fp/policyParser.js.map +1 -1
  110. package/dist/inboxRoutes.d.ts +22 -0
  111. package/dist/inboxRoutes.js +114 -0
  112. package/dist/inboxRoutes.js.map +1 -0
  113. package/dist/index.js +734 -144
  114. package/dist/index.js.map +1 -1
  115. package/dist/mcpRoutes.d.ts +37 -0
  116. package/dist/mcpRoutes.js +76 -0
  117. package/dist/mcpRoutes.js.map +1 -0
  118. package/dist/oauth.d.ts +3 -0
  119. package/dist/oauth.js +151 -26
  120. package/dist/oauth.js.map +1 -1
  121. package/dist/oauthRoutes.d.ts +32 -0
  122. package/dist/oauthRoutes.js +124 -0
  123. package/dist/oauthRoutes.js.map +1 -0
  124. package/dist/orchestrator/orchestratorBridge.js +2 -2
  125. package/dist/orchestrator/orchestratorBridge.js.map +1 -1
  126. package/dist/patchworkConfig.d.ts +7 -0
  127. package/dist/patchworkConfig.js.map +1 -1
  128. package/dist/pluginLoader.d.ts +12 -0
  129. package/dist/pluginLoader.js +43 -4
  130. package/dist/pluginLoader.js.map +1 -1
  131. package/dist/pluginWatcher.js +8 -3
  132. package/dist/pluginWatcher.js.map +1 -1
  133. package/dist/preToolUseHook.d.ts +12 -0
  134. package/dist/preToolUseHook.js +23 -0
  135. package/dist/preToolUseHook.js.map +1 -1
  136. package/dist/recipeOrchestration.d.ts +8 -0
  137. package/dist/recipeOrchestration.js +320 -39
  138. package/dist/recipeOrchestration.js.map +1 -1
  139. package/dist/recipeRoutes.d.ts +154 -0
  140. package/dist/recipeRoutes.js +1098 -0
  141. package/dist/recipeRoutes.js.map +1 -0
  142. package/dist/recipes/captureForRunlog.d.ts +27 -0
  143. package/dist/recipes/captureForRunlog.js +128 -0
  144. package/dist/recipes/captureForRunlog.js.map +1 -0
  145. package/dist/recipes/chainedRunner.d.ts +54 -3
  146. package/dist/recipes/chainedRunner.js +256 -36
  147. package/dist/recipes/chainedRunner.js.map +1 -1
  148. package/dist/recipes/compiler.js +3 -3
  149. package/dist/recipes/compiler.js.map +1 -1
  150. package/dist/recipes/detectSilentFail.d.ts +34 -0
  151. package/dist/recipes/detectSilentFail.js +105 -0
  152. package/dist/recipes/detectSilentFail.js.map +1 -0
  153. package/dist/recipes/installer.js +3 -3
  154. package/dist/recipes/installer.js.map +1 -1
  155. package/dist/recipes/manifest.js +21 -6
  156. package/dist/recipes/manifest.js.map +1 -1
  157. package/dist/recipes/migrationWarnings.d.ts +12 -0
  158. package/dist/recipes/migrationWarnings.js +44 -0
  159. package/dist/recipes/migrationWarnings.js.map +1 -0
  160. package/dist/recipes/replayRun.d.ts +62 -0
  161. package/dist/recipes/replayRun.js +97 -0
  162. package/dist/recipes/replayRun.js.map +1 -0
  163. package/dist/recipes/resolveRecipePath.d.ts +69 -0
  164. package/dist/recipes/resolveRecipePath.js +202 -0
  165. package/dist/recipes/resolveRecipePath.js.map +1 -0
  166. package/dist/recipes/scheduler.js +102 -11
  167. package/dist/recipes/scheduler.js.map +1 -1
  168. package/dist/recipes/schemaGenerator.js +3 -3
  169. package/dist/recipes/schemaGenerator.js.map +1 -1
  170. package/dist/recipes/toolRegistry.d.ts +5 -0
  171. package/dist/recipes/toolRegistry.js +9 -0
  172. package/dist/recipes/toolRegistry.js.map +1 -1
  173. package/dist/recipes/tools/asana.d.ts +16 -0
  174. package/dist/recipes/tools/asana.js +524 -0
  175. package/dist/recipes/tools/asana.js.map +1 -0
  176. package/dist/recipes/tools/discord.d.ts +18 -0
  177. package/dist/recipes/tools/discord.js +254 -0
  178. package/dist/recipes/tools/discord.js.map +1 -0
  179. package/dist/recipes/tools/file.d.ts +6 -0
  180. package/dist/recipes/tools/file.js +12 -8
  181. package/dist/recipes/tools/file.js.map +1 -1
  182. package/dist/recipes/tools/github.js +29 -4
  183. package/dist/recipes/tools/github.js.map +1 -1
  184. package/dist/recipes/tools/gitlab.d.ts +11 -0
  185. package/dist/recipes/tools/gitlab.js +285 -0
  186. package/dist/recipes/tools/gitlab.js.map +1 -0
  187. package/dist/recipes/tools/gmail.d.ts +1 -1
  188. package/dist/recipes/tools/gmail.js +230 -6
  189. package/dist/recipes/tools/gmail.js.map +1 -1
  190. package/dist/recipes/tools/googleDrive.d.ts +1 -0
  191. package/dist/recipes/tools/googleDrive.js +55 -0
  192. package/dist/recipes/tools/googleDrive.js.map +1 -0
  193. package/dist/recipes/tools/index.d.ts +8 -0
  194. package/dist/recipes/tools/index.js +8 -0
  195. package/dist/recipes/tools/index.js.map +1 -1
  196. package/dist/recipes/tools/jira.d.ts +14 -0
  197. package/dist/recipes/tools/jira.js +369 -0
  198. package/dist/recipes/tools/jira.js.map +1 -0
  199. package/dist/recipes/tools/linear.d.ts +2 -1
  200. package/dist/recipes/tools/linear.js +227 -3
  201. package/dist/recipes/tools/linear.js.map +1 -1
  202. package/dist/recipes/tools/meetingNotes.d.ts +21 -0
  203. package/dist/recipes/tools/meetingNotes.js +701 -0
  204. package/dist/recipes/tools/meetingNotes.js.map +1 -0
  205. package/dist/recipes/tools/pagerduty.d.ts +15 -0
  206. package/dist/recipes/tools/pagerduty.js +451 -0
  207. package/dist/recipes/tools/pagerduty.js.map +1 -0
  208. package/dist/recipes/tools/sentry.d.ts +12 -0
  209. package/dist/recipes/tools/sentry.js +73 -0
  210. package/dist/recipes/tools/sentry.js.map +1 -0
  211. package/dist/recipes/tools/slack.js +15 -5
  212. package/dist/recipes/tools/slack.js.map +1 -1
  213. package/dist/recipes/validation.js +83 -14
  214. package/dist/recipes/validation.js.map +1 -1
  215. package/dist/recipes/yamlRunner.d.ts +30 -2
  216. package/dist/recipes/yamlRunner.js +369 -70
  217. package/dist/recipes/yamlRunner.js.map +1 -1
  218. package/dist/recipesHttp.d.ts +76 -1
  219. package/dist/recipesHttp.js +474 -12
  220. package/dist/recipesHttp.js.map +1 -1
  221. package/dist/runLog.d.ts +78 -2
  222. package/dist/runLog.js +204 -6
  223. package/dist/runLog.js.map +1 -1
  224. package/dist/schemas/dry-run-plan.v1.json +139 -0
  225. package/dist/schemas/recipe.v1.json +684 -0
  226. package/dist/server.d.ts +79 -10
  227. package/dist/server.js +366 -1384
  228. package/dist/server.js.map +1 -1
  229. package/dist/ssrfGuard.d.ts +54 -0
  230. package/dist/ssrfGuard.js +122 -0
  231. package/dist/ssrfGuard.js.map +1 -0
  232. package/dist/streamableHttp.d.ts +39 -1
  233. package/dist/streamableHttp.js +126 -17
  234. package/dist/streamableHttp.js.map +1 -1
  235. package/dist/tools/getDocumentSymbols.d.ts +24 -0
  236. package/dist/tools/getDocumentSymbols.js +74 -8
  237. package/dist/tools/getDocumentSymbols.js.map +1 -1
  238. package/dist/tools/getSecurityAdvisories.js +10 -1
  239. package/dist/tools/getSecurityAdvisories.js.map +1 -1
  240. package/dist/tools/getSessionUsage.d.ts +3 -0
  241. package/dist/tools/getSessionUsage.js +3 -0
  242. package/dist/tools/getSessionUsage.js.map +1 -1
  243. package/dist/tools/index.d.ts +8 -0
  244. package/dist/tools/index.js +32 -2
  245. package/dist/tools/index.js.map +1 -1
  246. package/dist/tools/slackPostMessage.js +1 -1
  247. package/dist/tools/slackPostMessage.js.map +1 -1
  248. package/dist/tools/transaction.d.ts +19 -0
  249. package/dist/tools/transaction.js +29 -0
  250. package/dist/tools/transaction.js.map +1 -1
  251. package/dist/traceEncryption.d.ts +46 -0
  252. package/dist/traceEncryption.js +124 -0
  253. package/dist/traceEncryption.js.map +1 -0
  254. package/dist/transport.d.ts +39 -0
  255. package/dist/transport.js +88 -8
  256. package/dist/transport.js.map +1 -1
  257. package/package.json +22 -5
  258. package/templates/policies/README.md +72 -0
  259. package/templates/policies/conservative.json +14 -0
  260. package/templates/policies/developer.json +14 -0
  261. package/templates/policies/headless-ci.json +24 -0
  262. package/templates/policies/personal-assistant.json +15 -0
  263. package/templates/policies/regulated-industry.json +18 -0
  264. package/templates/recipes/project-health-check.yaml +1 -1
  265. package/templates/recipes/webhook/README.md +70 -0
  266. package/templates/recipes/webhook/capture-thought.yaml +26 -0
  267. package/templates/recipes/webhook/customer-escalation.yaml +49 -0
  268. package/templates/recipes/webhook/incident-intake.yaml +46 -0
  269. package/templates/recipes/webhook/meeting-prep.yaml +48 -0
  270. package/templates/recipes/webhook/morning-brief.yaml +57 -0
@@ -0,0 +1,512 @@
1
+ /**
2
+ * approvalSignals — passive risk personalization for the approval queue.
3
+ *
4
+ * Computes user-specific signals based on past approval / activity history.
5
+ * These supplement (not replace) the policy-engine `riskSignals` which are
6
+ * computed from the call's CONTENT (params shape, destructive flags, etc.).
7
+ *
8
+ * Personal signals describe the user's RELATIONSHIP to this tool/call:
9
+ * "you approved this 27 times" is a different signal class from "this
10
+ * command contains rm -rf". Both should reach the approval modal.
11
+ *
12
+ * Catalog (from docs/strategic/2026-05-02/memory-ecosystem-report.md §5):
13
+ * three of the twelve heuristics shipped here. Heuristics 4-12 follow.
14
+ *
15
+ * 1. "You approved similar actions N times" — past allow on same tool
16
+ * 2. "You rejected this tool before" — past deny on same tool
17
+ * 3. "First use of this connector" — connector namespace ∩ activity log
18
+ * 5. "Last called T days ago" — gap since most recent call (heuristic 4
19
+ * from the catalog is satisfied by the first_connector_use kind above)
20
+ * 7. "Risk tier escalation" — current tier exceeds the user's typical
21
+ * approved tier across recent decisions
22
+ * 6. "Mirrors a recipe step you trust" — primary param matches a step
23
+ * from a successful past recipe run
24
+ * 8. "Often runs alongside X" — co-occurrence pairing in recent activity
25
+ * 9. "Workspace mismatch" — call from a workspace that has never
26
+ * approved this tool before
27
+ * 10. "Time-of-day anomaly" — call hour outside the user's usual window
28
+ * for this tool (opt-in: gate via `enableTimeOfDayAnomaly`)
29
+ * 11. "Param novelty" — primary param (command/url/pattern) prefix never
30
+ * seen on prior approvals of this tool
31
+ * 12. "Cooldown breach" — same tool fired N+ times in a short window
32
+ *
33
+ * The signals are **transparent**: every signal has a `source` enum so a
34
+ * future "why is this signal here?" UI can link back to the rows that
35
+ * produced it. We do not infer; we count and match. No model, no
36
+ * fine-tuning. Honesty is the value proposition.
37
+ *
38
+ * Privacy: signals are computed locally over local logs. They flow into
39
+ * the approval queue's PendingApproval shape, which is exposed via
40
+ * GET /approvals (bearer-auth-gated) and the SSE stream (same auth).
41
+ * Nothing leaves the machine.
42
+ */
43
+ import { isConnectorNamespace } from "./recipes/toolRegistry.js";
44
+ /**
45
+ * Threshold under which heuristic 1 ("you approved this N times") does
46
+ * not surface. With < 3 prior approvals the signal is noise — it could
47
+ * be the first three exploratory calls a user always rubber-stamps. ≥ 3
48
+ * is "you have a pattern here."
49
+ */
50
+ const PRIOR_APPROVALS_THRESHOLD = 3;
51
+ /**
52
+ * Minimum gap before "you haven't used this in a while" surfaces. Under a
53
+ * week the user almost certainly remembers; past a week, the call may
54
+ * deserve a second look. Tunable, not load-bearing — change freely if
55
+ * dashboard feedback says it's noisy or too quiet.
56
+ */
57
+ const STALE_TOOL_DAYS = 7;
58
+ const STALE_TOOL_MS = STALE_TOOL_DAYS * 24 * 60 * 60 * 1_000;
59
+ /** Bumped severity threshold — past a month away is "have you forgotten what this does" territory. */
60
+ const STALE_TOOL_HIGH_DAYS = 30;
61
+ /**
62
+ * Minimum prior-allow decisions needed to establish a "typical tier"
63
+ * baseline for heuristic 7. Below this we don't claim to know the user's
64
+ * pattern — first few approvals could be anything. 5 is small but enough
65
+ * to make a single outlier not dominate.
66
+ */
67
+ const TIER_BASELINE_MIN_SAMPLES = 5;
68
+ const TIER_RANK = { low: 0, medium: 1, high: 2 };
69
+ /**
70
+ * Window for co-occurrence detection — 15 minutes lines up with the
71
+ * catalog's heuristic 8 description and is wide enough to capture an
72
+ * "I'm in the middle of doing X" workflow without crossing session
73
+ * boundaries.
74
+ */
75
+ const COOCCURRENCE_WINDOW_MS = 15 * 60 * 1_000;
76
+ /** Minimum co-occurrence count before the chip surfaces. < 3 is noise. */
77
+ const COOCCURRENCE_MIN_COUNT = 3;
78
+ /**
79
+ * Cooldown-breach window — five minutes is short enough that "fired N
80
+ * times" is a behavioral pattern (a runaway loop, a panicked retry, a
81
+ * stuck recipe) rather than ordinary use. ApprovalQueue.inflightKey
82
+ * already deduplicates concurrent identical requests; this surfaces the
83
+ * cumulative pattern that inflight dedup hides.
84
+ */
85
+ const COOLDOWN_WINDOW_MS = 5 * 60 * 1_000;
86
+ /** Min repeat count inside the window before the chip surfaces. */
87
+ const COOLDOWN_BREACH_MIN = 3;
88
+ /** Severity bumps to high once the burst is unmistakable. */
89
+ const COOLDOWN_BREACH_HIGH = 6;
90
+ /**
91
+ * Minimum prior calls (any hour) before the time-of-day histogram is
92
+ * trustworthy. Below this we don't claim to know the user's pattern —
93
+ * any single hour could legitimately be the first time the tool runs.
94
+ */
95
+ const TIME_OF_DAY_BASELINE_MIN = 10;
96
+ /** Most recent calls considered when building the per-hour histogram. */
97
+ const TIME_OF_DAY_LOOKBACK = 200;
98
+ /**
99
+ * Tools whose calls have a single high-signal "primary param" that's
100
+ * worth tracking for novelty. Map value = the param key. Other tools
101
+ * skip h11 entirely — generic approval-modal fields like "specifier"
102
+ * are too noisy to compare across calls.
103
+ */
104
+ const PARAM_NOVELTY_PRIMARY_KEYS = {
105
+ runCommand: "command",
106
+ Bash: "command",
107
+ sendHttpRequest: "url",
108
+ WebFetch: "url",
109
+ searchAndReplace: "pattern",
110
+ };
111
+ /**
112
+ * Number of leading characters used as the prefix key for novelty
113
+ * comparison. Long enough to distinguish `git push` from `git pull` but
114
+ * short enough that param tail variation (file paths, sha args) doesn't
115
+ * make every call look novel.
116
+ */
117
+ const PARAM_NOVELTY_PREFIX_LEN = 24;
118
+ /** Minimum prior calls before the novelty signal is trustworthy. */
119
+ const PARAM_NOVELTY_BASELINE_MIN = 5;
120
+ /**
121
+ * How many recent successful recipe runs we scan for matching step
122
+ * results in heuristic 6. 200 is enough to catch any "you run this
123
+ * recipe daily" pattern without unbounded scan cost.
124
+ */
125
+ const RECIPE_STEP_TRUST_LOOKBACK = 200;
126
+ /**
127
+ * Minimum number of matching successful runs before we surface the
128
+ * "trusted recipe step" signal. ≥ 2 means "this isn't a one-off."
129
+ */
130
+ const RECIPE_STEP_TRUST_MIN = 2;
131
+ /**
132
+ * Compute the personal-signal set for an incoming approval request.
133
+ *
134
+ * Pure function over the activity log; no I/O of its own. Tested in
135
+ * isolation by feeding a mock ActivityLog. The activityLog argument is
136
+ * passed positionally rather than wired through deps so the surface
137
+ * stays inspectable.
138
+ */
139
+ export function computePersonalSignals(input) {
140
+ const { toolName, activityLog, currentTier, currentWorkspace, enableTimeOfDayAnomaly, currentParams, recipeRunLog, } = input;
141
+ if (!toolName)
142
+ return [];
143
+ const signals = [];
144
+ // Heuristic 1: "You approved this N times before."
145
+ // Surface only when count crosses PRIOR_APPROVALS_THRESHOLD so we don't
146
+ // flag every second call. Cap the message at three buckets for legibility.
147
+ const priorApprovals = activityLog.queryApprovalDecisions({
148
+ toolName,
149
+ decision: "allow",
150
+ });
151
+ if (priorApprovals.length >= PRIOR_APPROVALS_THRESHOLD) {
152
+ signals.push({
153
+ kind: "prior_approvals",
154
+ label: priorApprovalsLabel(priorApprovals.length),
155
+ severity: "low",
156
+ source: "approval_history",
157
+ count: priorApprovals.length,
158
+ });
159
+ }
160
+ // Heuristic 2: "You rejected this tool before."
161
+ // Any prior rejection is signal — explicit rejections are rare and
162
+ // intentional. Severity scales with how recent and how many.
163
+ const priorRejections = activityLog.queryApprovalDecisions({
164
+ toolName,
165
+ decision: "deny",
166
+ });
167
+ if (priorRejections.length > 0) {
168
+ signals.push({
169
+ kind: "prior_rejection",
170
+ label: priorRejectionLabel(priorRejections.length),
171
+ severity: priorRejections.length >= 2 ? "high" : "medium",
172
+ source: "approval_history",
173
+ count: priorRejections.length,
174
+ });
175
+ }
176
+ // Heuristic 3: "First use of this connector" / "first use of this tool".
177
+ // For namespaced tool ids only (`namespace.subtool` shape). Two flavors:
178
+ // - If the namespace is in the connector registry → "first connector use"
179
+ // (high salience: connectors hit external services with credentials).
180
+ // - Otherwise, plain "first use" with low salience.
181
+ // We require a namespaced tool id so we don't flag every CLI tool ever
182
+ // called with no prior history (which would fire on every fresh session).
183
+ const namespace = toolName.split(".")[0];
184
+ if (namespace && namespace !== toolName) {
185
+ const priorInNamespace = activityLog.queryByNamespace(namespace, 50);
186
+ if (priorInNamespace.length === 0) {
187
+ const isConnector = isConnectorNamespace(namespace);
188
+ signals.push({
189
+ kind: isConnector ? "first_connector_use" : "first_tool_use",
190
+ label: isConnector
191
+ ? `First use of the ${namespace} connector — credentials and external calls about to fire.`
192
+ : `First use of any ${namespace}.* tool in this workspace.`,
193
+ severity: isConnector ? "high" : "low",
194
+ source: "activity_history",
195
+ });
196
+ }
197
+ }
198
+ // Heuristic 5: "Last called T days ago."
199
+ // Surfaces a low/medium signal when the user hasn't called this exact
200
+ // tool in a while. The message gives the user a beat to reconsider —
201
+ // memory of *why* a call was approved fades faster than the data does.
202
+ // No signal at all when there's no prior call (heuristic 3 handles
203
+ // first-use); no signal when the gap is under STALE_TOOL_DAYS.
204
+ const lastCall = activityLog.queryLastToolCall(toolName);
205
+ if (lastCall) {
206
+ const lastMs = Date.parse(lastCall.timestamp);
207
+ const gapMs = Number.isFinite(lastMs) ? Date.now() - lastMs : 0;
208
+ if (gapMs >= STALE_TOOL_MS) {
209
+ const days = Math.floor(gapMs / (24 * 60 * 60 * 1_000));
210
+ signals.push({
211
+ kind: "stale_tool_use",
212
+ label: staleToolLabel(days),
213
+ severity: days >= STALE_TOOL_HIGH_DAYS ? "medium" : "low",
214
+ source: "activity_history",
215
+ count: days,
216
+ });
217
+ }
218
+ }
219
+ // Heuristic 7: "Risk tier escalation."
220
+ // Compare the incoming call's tier against the user's typical approved
221
+ // tier across recent allow-decisions. If the user usually approves low
222
+ // and this is medium/high — or usually medium and this is high — surface.
223
+ // Cross-tool: the question is "is this user's threshold being exceeded",
224
+ // not "is this tool a step up from this tool's history". The latter is
225
+ // covered by heuristic 1 (prior approvals on the same tool).
226
+ if (currentTier) {
227
+ const recentAllows = activityLog
228
+ .queryApprovalDecisions({ decision: "allow", last: 50 })
229
+ .filter((e) => e.metadata?.tier === "low" ||
230
+ e.metadata?.tier === "medium" ||
231
+ e.metadata?.tier === "high");
232
+ if (recentAllows.length >= TIER_BASELINE_MIN_SAMPLES) {
233
+ // p50 over rank space — sort ranks, pick the middle. Equivalent to
234
+ // median; cheaper than a full distribution and stable on small N.
235
+ const ranks = recentAllows
236
+ .map((e) => TIER_RANK[e.metadata.tier])
237
+ .sort((a, b) => a - b);
238
+ const baselineRank = ranks[Math.floor(ranks.length / 2)] ?? 0;
239
+ const currentRank = TIER_RANK[currentTier];
240
+ if (currentRank > baselineRank) {
241
+ const baselineTier = Object.keys(TIER_RANK).find((t) => TIER_RANK[t] === baselineRank);
242
+ const jump = currentRank - baselineRank;
243
+ signals.push({
244
+ kind: "tier_escalation",
245
+ label: tierEscalationLabel(baselineTier ?? "low", currentTier),
246
+ // jump of 1 (low→med, med→high) = medium; jump of 2 (low→high) = high
247
+ severity: jump >= 2 ? "high" : "medium",
248
+ source: "approval_history",
249
+ });
250
+ }
251
+ }
252
+ }
253
+ // Heuristic 8: "Often runs alongside X."
254
+ // Informational chip — when the user calls this tool it tends to run
255
+ // near another tool (within 15min). Surfaces the strongest partner.
256
+ // Distinct from heuristic 1 (history of THIS tool); this signal is
257
+ // about the workflow shape: "you're probably mid-deploy" / "this is
258
+ // your morning-brief sequence". Severity always low — this is context,
259
+ // not warning. Catalog flags this as medium-FP, so we underclaim.
260
+ const pairs = activityLog.coOccurrence(COOCCURRENCE_WINDOW_MS);
261
+ let bestPartner = null;
262
+ for (const { pair, count } of pairs) {
263
+ if (count < COOCCURRENCE_MIN_COUNT)
264
+ continue;
265
+ const [a, b] = pair.split("|");
266
+ const partner = a === toolName ? b : b === toolName ? a : null;
267
+ if (!partner)
268
+ continue;
269
+ // pairs is sorted by count desc, so the first match is the strongest.
270
+ bestPartner = { name: partner, count };
271
+ break;
272
+ }
273
+ if (bestPartner) {
274
+ signals.push({
275
+ kind: "cooccurrence_pattern",
276
+ label: `Often runs alongside ${bestPartner.name} (${bestPartner.count} co-occurrences in your recent activity).`,
277
+ severity: "low",
278
+ source: "activity_history",
279
+ count: bestPartner.count,
280
+ });
281
+ }
282
+ // Heuristic 9: "Workspace mismatch."
283
+ // Surfaces when this tool has been approved (allow OR deny — any
284
+ // human decision counts as a "this workspace has seen this tool"
285
+ // signal) in other workspaces but not in the one the call is coming
286
+ // from. Catches "I just opened a new project and the agent wants to
287
+ // run the same risky tool — fresh workspace, no consent record."
288
+ // Skipped when currentWorkspace is missing (test fixtures, callers
289
+ // without a workspace context).
290
+ if (currentWorkspace) {
291
+ const allDecisions = activityLog.queryApprovalDecisions({ toolName });
292
+ const seenWorkspaces = new Set();
293
+ let priorWithWorkspace = 0;
294
+ for (const e of allDecisions) {
295
+ const ws = e.metadata?.workspace;
296
+ if (typeof ws === "string" && ws.length > 0) {
297
+ seenWorkspaces.add(ws);
298
+ priorWithWorkspace++;
299
+ }
300
+ }
301
+ // Need ≥ 1 prior decision *with workspace metadata* to claim a
302
+ // baseline. Older rows lacking the field can't tell us anything.
303
+ if (priorWithWorkspace > 0 && !seenWorkspaces.has(currentWorkspace)) {
304
+ signals.push({
305
+ kind: "workspace_mismatch",
306
+ label: workspaceMismatchLabel(seenWorkspaces.size),
307
+ // Catalog says "FP low" — workspace is a strong intent boundary.
308
+ // But it's still informational rather than a hard warning.
309
+ severity: "medium",
310
+ source: "approval_history",
311
+ count: seenWorkspaces.size,
312
+ });
313
+ }
314
+ }
315
+ // Heuristic 10: "Time-of-day anomaly."
316
+ // Build a 24-bucket hour-of-day histogram for past calls of this tool.
317
+ // If the current hour has zero prior calls AND total prior calls is
318
+ // past the baseline, surface as informational (low severity). Catalog
319
+ // flags this as medium-FP so it stays opt-in and never escalates to
320
+ // medium/high — purely a "huh, that's unusual" chip.
321
+ if (enableTimeOfDayAnomaly) {
322
+ const recent = activityLog.query({
323
+ tool: toolName,
324
+ last: TIME_OF_DAY_LOOKBACK,
325
+ });
326
+ if (recent.length >= TIME_OF_DAY_BASELINE_MIN) {
327
+ const hourCounts = new Array(24).fill(0);
328
+ for (const e of recent) {
329
+ const hr = new Date(e.timestamp).getHours();
330
+ if (Number.isFinite(hr) && hr >= 0 && hr < 24) {
331
+ hourCounts[hr] = (hourCounts[hr] ?? 0) + 1;
332
+ }
333
+ }
334
+ const currentHour = new Date().getHours();
335
+ if ((hourCounts[currentHour] ?? 0) === 0) {
336
+ signals.push({
337
+ kind: "time_of_day_anomaly",
338
+ label: timeOfDayLabel(currentHour, recent.length),
339
+ severity: "low",
340
+ source: "activity_history",
341
+ count: recent.length,
342
+ });
343
+ }
344
+ }
345
+ }
346
+ // Heuristic 11: "Param novelty."
347
+ // For tools with a clear primary string param (runCommand → command,
348
+ // sendHttpRequest → url, etc.), build a Set of past param prefixes
349
+ // from approval_decision metadata. If the current call's prefix isn't
350
+ // in that set AND we have enough baseline, surface as informational.
351
+ // Compares only the leading PARAM_NOVELTY_PREFIX_LEN chars so the
352
+ // tail (file paths, sha args, query params) doesn't make every call
353
+ // look novel.
354
+ const primaryKey = PARAM_NOVELTY_PRIMARY_KEYS[toolName];
355
+ if (primaryKey && currentParams) {
356
+ const currentRaw = currentParams[primaryKey];
357
+ if (typeof currentRaw === "string" && currentRaw.length > 0) {
358
+ const currentPrefix = paramPrefix(currentRaw);
359
+ const priorDecisions = activityLog.queryApprovalDecisions({
360
+ toolName,
361
+ });
362
+ const seenPrefixes = new Set();
363
+ for (const e of priorDecisions) {
364
+ const params = e.metadata?.params;
365
+ if (typeof params !== "object" || params === null)
366
+ continue;
367
+ const v = params[primaryKey];
368
+ if (typeof v === "string" && v.length > 0) {
369
+ seenPrefixes.add(paramPrefix(v));
370
+ }
371
+ }
372
+ if (seenPrefixes.size >= PARAM_NOVELTY_BASELINE_MIN &&
373
+ !seenPrefixes.has(currentPrefix)) {
374
+ signals.push({
375
+ kind: "param_novelty",
376
+ label: paramNoveltyLabel(primaryKey, seenPrefixes.size),
377
+ severity: "medium",
378
+ source: "approval_history",
379
+ count: seenPrefixes.size,
380
+ });
381
+ }
382
+ }
383
+ }
384
+ // Heuristic 6: "Mirrors a recipe step you trust."
385
+ // Scan recent SUCCESSFUL recipe runs for a step whose tool matches
386
+ // and whose resolvedParams primary key shares the same 24-char prefix
387
+ // as the current call. If we find ≥ RECIPE_STEP_TRUST_MIN such runs,
388
+ // surface the recipe name and the count. Reuses h11's primary-key
389
+ // map so "this tool needs a primary param to compare" is one decision.
390
+ // Trust signal — low severity (informational reassurance, not warning).
391
+ const recipePrimaryKey = PARAM_NOVELTY_PRIMARY_KEYS[toolName];
392
+ if (recipeRunLog && recipePrimaryKey && currentParams) {
393
+ const currentRaw = currentParams[recipePrimaryKey];
394
+ if (typeof currentRaw === "string" && currentRaw.length > 0) {
395
+ const currentPrefix = paramPrefix(currentRaw);
396
+ const recentRuns = recipeRunLog.query({
397
+ status: "done",
398
+ limit: RECIPE_STEP_TRUST_LOOKBACK,
399
+ });
400
+ const matchingRecipes = new Map();
401
+ for (const run of recentRuns) {
402
+ if (!run.stepResults)
403
+ continue;
404
+ for (const step of run.stepResults) {
405
+ if (step.tool !== toolName || step.status !== "ok")
406
+ continue;
407
+ const sp = step.resolvedParams;
408
+ if (typeof sp !== "object" || sp === null)
409
+ continue;
410
+ const v = sp[recipePrimaryKey];
411
+ if (typeof v === "string" &&
412
+ v.length > 0 &&
413
+ paramPrefix(v) === currentPrefix) {
414
+ matchingRecipes.set(run.recipeName, (matchingRecipes.get(run.recipeName) ?? 0) + 1);
415
+ // One match per run is enough — same step run repeatedly in
416
+ // a single recipe execution doesn't add weight.
417
+ break;
418
+ }
419
+ }
420
+ }
421
+ let totalMatches = 0;
422
+ let topRecipe = null;
423
+ let topCount = 0;
424
+ for (const [name, count] of matchingRecipes) {
425
+ totalMatches += count;
426
+ if (count > topCount) {
427
+ topCount = count;
428
+ topRecipe = name;
429
+ }
430
+ }
431
+ if (totalMatches >= RECIPE_STEP_TRUST_MIN && topRecipe) {
432
+ signals.push({
433
+ kind: "recipe_step_trust",
434
+ label: recipeStepTrustLabel(topRecipe, totalMatches),
435
+ severity: "low",
436
+ source: "recipe_run_log",
437
+ count: totalMatches,
438
+ });
439
+ }
440
+ }
441
+ }
442
+ // Heuristic 12: "Cooldown breach."
443
+ // Same tool fired N+ times within a short window — pattern of a runaway
444
+ // loop, panicked retry, or stuck recipe. Distinct from heuristic 1
445
+ // (long-tail history): h1 says "you usually approve this", h12 says
446
+ // "you're approving this RIGHT NOW more than usual." Approvals modal
447
+ // should show both when both fire.
448
+ const recentCalls = activityLog.query({ tool: toolName, last: 50 });
449
+ const cutoff = Date.now() - COOLDOWN_WINDOW_MS;
450
+ const burstCount = recentCalls.filter((e) => Date.parse(e.timestamp) >= cutoff).length;
451
+ if (burstCount >= COOLDOWN_BREACH_MIN) {
452
+ signals.push({
453
+ kind: "cooldown_breach",
454
+ label: cooldownBreachLabel(burstCount),
455
+ severity: burstCount >= COOLDOWN_BREACH_HIGH ? "high" : "medium",
456
+ source: "activity_history",
457
+ count: burstCount,
458
+ });
459
+ }
460
+ return signals;
461
+ }
462
+ function workspaceMismatchLabel(otherCount) {
463
+ if (otherCount === 1) {
464
+ return "Approved in a different workspace before — first time you've allowed it here.";
465
+ }
466
+ return `Approved in ${otherCount} other workspaces before — first time you've allowed it here.`;
467
+ }
468
+ function paramPrefix(s) {
469
+ // Trim leading whitespace so " git push" and "git push" hash the same.
470
+ return s.trimStart().slice(0, PARAM_NOVELTY_PREFIX_LEN);
471
+ }
472
+ function recipeStepTrustLabel(recipeName, totalMatches) {
473
+ return `Matches a step in your ${recipeName} recipe (${totalMatches} successful runs).`;
474
+ }
475
+ function paramNoveltyLabel(paramKey, baselineSize) {
476
+ return `Novel ${paramKey} prefix — across ${baselineSize} prior approvals you've never run this exact shape.`;
477
+ }
478
+ function timeOfDayLabel(hour, baselineCount) {
479
+ const hh = hour.toString().padStart(2, "0");
480
+ return `First call at ${hh}:00 — outside your usual hours for this tool (across ${baselineCount} prior calls).`;
481
+ }
482
+ function cooldownBreachLabel(count) {
483
+ return `Called ${count} times in the last 5 minutes — possible runaway loop or panicked retry.`;
484
+ }
485
+ function tierEscalationLabel(baseline, current) {
486
+ return `You usually approve ${baseline}-tier calls — this one is ${current}.`;
487
+ }
488
+ function priorApprovalsLabel(count) {
489
+ if (count >= 100) {
490
+ return `You've approved this tool ${count}+ times — well-trusted in your workflow.`;
491
+ }
492
+ if (count >= 20) {
493
+ return `You've approved this tool ${count} times before.`;
494
+ }
495
+ return `You've approved this tool ${count} times before.`;
496
+ }
497
+ function staleToolLabel(days) {
498
+ if (days >= 365)
499
+ return `Last called over a year ago (${days} days).`;
500
+ if (days >= 30) {
501
+ const months = Math.floor(days / 30);
502
+ return `Last called ${months === 1 ? "a month" : `${months} months`} ago — context may have changed since.`;
503
+ }
504
+ return `Last called ${days} days ago.`;
505
+ }
506
+ function priorRejectionLabel(count) {
507
+ if (count === 1) {
508
+ return "You rejected this tool once before — context may have changed since.";
509
+ }
510
+ return `You've rejected this tool ${count} times before — recurring pattern of caution.`;
511
+ }
512
+ //# sourceMappingURL=approvalSignals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approvalSignals.js","sourceRoot":"","sources":["../src/approvalSignals.ts"],"names":[],"mappings":"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"}
@@ -506,6 +506,43 @@ export declare class AutomationHooks {
506
506
  /** Last interpreter run promise — allows tests to await completion. */
507
507
  private _lastRunPromise;
508
508
  constructor(policy: AutomationPolicy, orchestrator: ClaudeOrchestrator, log: (msg: string) => void, _extensionClient?: ExtensionClient, _workspace?: string);
509
+ /**
510
+ * Tracks whether `_lastRunPromise` represents a still-running interpreter
511
+ * call. When false, we know the prior promise is settled and can start the
512
+ * next run synchronously (preserving start-sync semantics tests depend on).
513
+ * When true, we chain via `.then()` so the new run reads the post-state of
514
+ * the prior one — fixing the race where two concurrent events both observed
515
+ * stale `_automationState` and the second writer clobbered the first.
516
+ */
517
+ private _chainBusy;
518
+ /**
519
+ * Enqueue an interpreter run, serializing it after any prior in-flight run.
520
+ *
521
+ * Concurrent events (e.g. a file save and a diagnostic update in the same
522
+ * tick) used to overwrite `_lastRunPromise`, so both runs read the same
523
+ * stale `_automationState` and the second writer's `updatedState` clobbered
524
+ * the first's. Cooldown/dedup writes from one event were silently lost;
525
+ * orphaned tasks accumulated in `activeTasks`.
526
+ *
527
+ * Chain through `_lastRunPromise` so each run sees the post-state of the
528
+ * previous one. We start the next run synchronously when the chain is idle
529
+ * (preserves the contract that `_runInterpreter` body executes up to its
530
+ * first await before this call returns); only when a run is in flight do
531
+ * we defer via `.then()`.
532
+ */
533
+ private _enqueueRun;
534
+ /**
535
+ * Enqueue a synchronous state mutation to run after any in-flight
536
+ * interpreter call. Without this, handler-side `_automationState =
537
+ * setLatestDiagnostics(...)` writes done synchronously between
538
+ * `_enqueueRun` calls would be overwritten when the first run's
539
+ * `result.value.updatedState` writeback fires (the writeback replaces
540
+ * the full state, including the handler's incremental change).
541
+ *
542
+ * When the chain is idle, run the mutation synchronously to preserve
543
+ * test ergonomics that assume state is observable on the next line.
544
+ */
545
+ private _enqueueMutation;
509
546
  private _runInterpreter;
510
547
  /**
511
548
  * Returns a Promise that resolves once all in-flight interpreter runs finish.