@probelabs/visor 0.1.107 → 0.1.112

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 (235) hide show
  1. package/README.md +6 -0
  2. package/defaults/task-refinement.yaml +7 -3
  3. package/defaults/visor.tests.yaml +13 -2
  4. package/defaults/visor.yaml +1 -0
  5. package/dist/663.index.js +3 -2
  6. package/dist/80.index.js +3 -2
  7. package/dist/ai-review-service.d.ts +13 -9
  8. package/dist/ai-review-service.d.ts.map +1 -1
  9. package/dist/cli-main.d.ts.map +1 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/debug-visualizer/ws-server.d.ts +7 -1
  13. package/dist/debug-visualizer/ws-server.d.ts.map +1 -1
  14. package/dist/defaults/task-refinement.yaml +7 -3
  15. package/dist/defaults/visor.tests.yaml +13 -2
  16. package/dist/defaults/visor.yaml +1 -0
  17. package/dist/docs/advanced-ai.md +60 -1
  18. package/dist/docs/ai-configuration.md +67 -0
  19. package/dist/docs/ai-custom-tools-usage.md +261 -0
  20. package/dist/docs/ai-custom-tools.md +392 -0
  21. package/dist/docs/bot-transports-rfc.md +23 -0
  22. package/dist/docs/configuration.md +21 -0
  23. package/dist/docs/engine-pause-resume-rfc.md +192 -0
  24. package/dist/docs/lifecycle-hooks.md +253 -0
  25. package/dist/docs/liquid-templates.md +143 -0
  26. package/dist/docs/providers/git-checkout.md +589 -0
  27. package/dist/docs/recipes.md +458 -5
  28. package/dist/docs/rfc/git-checkout-step.md +601 -0
  29. package/dist/docs/rfc/on_init-hook.md +1294 -0
  30. package/dist/docs/rfc/workspace-isolation.md +216 -0
  31. package/dist/docs/router-patterns.md +339 -0
  32. package/dist/event-bus/types.d.ts +14 -0
  33. package/dist/event-bus/types.d.ts.map +1 -1
  34. package/dist/examples/ai-custom-tools-example.yaml +206 -0
  35. package/dist/examples/ai-custom-tools-simple.yaml +76 -0
  36. package/dist/examples/git-checkout-basic.yaml +32 -0
  37. package/dist/examples/git-checkout-compare.yaml +59 -0
  38. package/dist/examples/git-checkout-cross-repo.yaml +76 -0
  39. package/dist/examples/on-init-import-demo.yaml +179 -0
  40. package/dist/examples/reusable-tools.yaml +92 -0
  41. package/dist/examples/reusable-workflows.yaml +88 -0
  42. package/dist/examples/session-reuse-self.yaml +81 -0
  43. package/dist/examples/slack-simple-chat.yaml +775 -0
  44. package/dist/failure-condition-evaluator.d.ts +2 -0
  45. package/dist/failure-condition-evaluator.d.ts.map +1 -1
  46. package/dist/frontends/github-frontend.d.ts +20 -0
  47. package/dist/frontends/github-frontend.d.ts.map +1 -1
  48. package/dist/frontends/host.d.ts +4 -0
  49. package/dist/frontends/host.d.ts.map +1 -1
  50. package/dist/frontends/slack-frontend.d.ts +58 -0
  51. package/dist/frontends/slack-frontend.d.ts.map +1 -0
  52. package/dist/generated/config-schema.d.ts +409 -41
  53. package/dist/generated/config-schema.d.ts.map +1 -1
  54. package/dist/generated/config-schema.json +436 -47
  55. package/dist/github-comments.d.ts +2 -0
  56. package/dist/github-comments.d.ts.map +1 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +83587 -56085
  59. package/dist/liquid-extensions.d.ts.map +1 -1
  60. package/dist/logger.d.ts +1 -0
  61. package/dist/logger.d.ts.map +1 -1
  62. package/dist/output/traces/{run-2025-11-21T11-50-46-505Z.ndjson → run-2026-01-21T05-37-24-446Z.ndjson} +91 -91
  63. package/dist/output/traces/run-2026-01-21T05-38-18-580Z.ndjson +1067 -0
  64. package/dist/output-formatters.d.ts.map +1 -1
  65. package/dist/providers/ai-check-provider.d.ts +12 -0
  66. package/dist/providers/ai-check-provider.d.ts.map +1 -1
  67. package/dist/providers/check-provider-registry.d.ts.map +1 -1
  68. package/dist/providers/check-provider.interface.d.ts +9 -0
  69. package/dist/providers/check-provider.interface.d.ts.map +1 -1
  70. package/dist/providers/command-check-provider.d.ts.map +1 -1
  71. package/dist/providers/custom-tool-executor.d.ts.map +1 -1
  72. package/dist/providers/git-checkout-provider.d.ts +25 -0
  73. package/dist/providers/git-checkout-provider.d.ts.map +1 -0
  74. package/dist/providers/http-client-provider.d.ts +3 -0
  75. package/dist/providers/http-client-provider.d.ts.map +1 -1
  76. package/dist/providers/human-input-check-provider.d.ts +2 -0
  77. package/dist/providers/human-input-check-provider.d.ts.map +1 -1
  78. package/dist/providers/log-check-provider.d.ts.map +1 -1
  79. package/dist/providers/mcp-check-provider.d.ts +1 -1
  80. package/dist/providers/mcp-check-provider.d.ts.map +1 -1
  81. package/dist/providers/mcp-custom-sse-server.d.ts +66 -0
  82. package/dist/providers/mcp-custom-sse-server.d.ts.map +1 -0
  83. package/dist/providers/memory-check-provider.d.ts.map +1 -1
  84. package/dist/providers/script-check-provider.d.ts.map +1 -1
  85. package/dist/providers/workflow-check-provider.d.ts.map +1 -1
  86. package/dist/reviewer.d.ts.map +1 -1
  87. package/dist/sdk/check-provider-registry-534KL5HT.mjs +27 -0
  88. package/dist/sdk/chunk-23L3QRYX.mjs +16872 -0
  89. package/dist/sdk/chunk-23L3QRYX.mjs.map +1 -0
  90. package/dist/sdk/{chunk-OOZITMRU.mjs → chunk-3OMWVM6J.mjs} +11 -1
  91. package/dist/sdk/{chunk-OOZITMRU.mjs.map → chunk-3OMWVM6J.mjs.map} +1 -1
  92. package/dist/sdk/{chunk-37ZSCMFC.mjs → chunk-7UK3NIIT.mjs} +2 -2
  93. package/dist/sdk/{chunk-VMPLF6FT.mjs → chunk-AGIZJ4UZ.mjs} +50 -4
  94. package/dist/sdk/chunk-AGIZJ4UZ.mjs.map +1 -0
  95. package/dist/sdk/{chunk-IEO6CFLG.mjs → chunk-AIVFBIS4.mjs} +161 -5
  96. package/dist/sdk/chunk-AIVFBIS4.mjs.map +1 -0
  97. package/dist/sdk/chunk-AK6BVWIT.mjs +426 -0
  98. package/dist/sdk/chunk-AK6BVWIT.mjs.map +1 -0
  99. package/dist/sdk/chunk-AUT26LHW.mjs +139 -0
  100. package/dist/sdk/chunk-AUT26LHW.mjs.map +1 -0
  101. package/dist/sdk/chunk-BOVFH3LI.mjs +232 -0
  102. package/dist/sdk/chunk-BOVFH3LI.mjs.map +1 -0
  103. package/dist/sdk/chunk-HTOKWMPO.mjs +157 -0
  104. package/dist/sdk/chunk-HTOKWMPO.mjs.map +1 -0
  105. package/dist/sdk/{chunk-6Y4YTKCF.mjs → chunk-NAW3DB3I.mjs} +2 -2
  106. package/dist/sdk/{chunk-OWUVOILT.mjs → chunk-QR7MOMJH.mjs} +4 -3
  107. package/dist/sdk/{chunk-OWUVOILT.mjs.map → chunk-QR7MOMJH.mjs.map} +1 -1
  108. package/dist/sdk/{chunk-PTL3K3PN.mjs → chunk-QY2XYPEV.mjs} +488 -60
  109. package/dist/sdk/chunk-QY2XYPEV.mjs.map +1 -0
  110. package/dist/sdk/{chunk-OZJ263FM.mjs → chunk-SIWNBRTK.mjs} +29 -215
  111. package/dist/sdk/chunk-SIWNBRTK.mjs.map +1 -0
  112. package/dist/sdk/command-executor-TYUV6HUS.mjs +14 -0
  113. package/dist/sdk/{config-M4ZNO6NU.mjs → config-YNC2EOOT.mjs} +5 -3
  114. package/dist/sdk/{failure-condition-evaluator-NBO5YRXW.mjs → failure-condition-evaluator-YGTF2GHG.mjs} +6 -5
  115. package/dist/sdk/{github-frontend-4AWRJT7D.mjs → github-frontend-SIAEOCON.mjs} +190 -12
  116. package/dist/sdk/github-frontend-SIAEOCON.mjs.map +1 -0
  117. package/dist/sdk/{host-7GBC3S7L.mjs → host-DXUYTNMU.mjs} +5 -2
  118. package/dist/sdk/host-DXUYTNMU.mjs.map +1 -0
  119. package/dist/sdk/{liquid-extensions-C7EG3YKH.mjs → liquid-extensions-PKWCKK7E.mjs} +5 -4
  120. package/dist/sdk/memory-store-XGBB7LX7.mjs +12 -0
  121. package/dist/sdk/prompt-state-YRJY6QAL.mjs +16 -0
  122. package/dist/sdk/{renderer-schema-6RF26VUS.mjs → renderer-schema-LPKN5UJS.mjs} +3 -2
  123. package/dist/sdk/{renderer-schema-6RF26VUS.mjs.map → renderer-schema-LPKN5UJS.mjs.map} +1 -1
  124. package/dist/sdk/{routing-RP56JTV2.mjs → routing-6N45MJ4F.mjs} +7 -6
  125. package/dist/sdk/sdk.d.mts +219 -5
  126. package/dist/sdk/sdk.d.ts +219 -5
  127. package/dist/sdk/sdk.js +21329 -14908
  128. package/dist/sdk/sdk.js.map +1 -1
  129. package/dist/sdk/sdk.mjs +407 -12874
  130. package/dist/sdk/sdk.mjs.map +1 -1
  131. package/dist/sdk/{session-registry-N5FFYFTM.mjs → session-registry-4E6YRQ77.mjs} +2 -2
  132. package/dist/sdk/session-registry-4E6YRQ77.mjs.map +1 -0
  133. package/dist/sdk/slack-frontend-BVKW3GD5.mjs +735 -0
  134. package/dist/sdk/slack-frontend-BVKW3GD5.mjs.map +1 -0
  135. package/dist/sdk/{tracer-init-WP4X46IF.mjs → tracer-init-GSLPPLCD.mjs} +2 -2
  136. package/dist/sdk/tracer-init-GSLPPLCD.mjs.map +1 -0
  137. package/dist/sdk/workflow-registry-R6KSACFR.mjs +12 -0
  138. package/dist/sdk/workflow-registry-R6KSACFR.mjs.map +1 -0
  139. package/dist/slack/adapter.d.ts +36 -0
  140. package/dist/slack/adapter.d.ts.map +1 -0
  141. package/dist/slack/cache-prewarmer.d.ts +31 -0
  142. package/dist/slack/cache-prewarmer.d.ts.map +1 -0
  143. package/dist/slack/client.d.ts +77 -0
  144. package/dist/slack/client.d.ts.map +1 -0
  145. package/dist/slack/markdown.d.ts +45 -0
  146. package/dist/slack/markdown.d.ts.map +1 -0
  147. package/dist/slack/prompt-state.d.ts +33 -0
  148. package/dist/slack/prompt-state.d.ts.map +1 -0
  149. package/dist/slack/rate-limiter.d.ts +56 -0
  150. package/dist/slack/rate-limiter.d.ts.map +1 -0
  151. package/dist/slack/signature.d.ts +2 -0
  152. package/dist/slack/signature.d.ts.map +1 -0
  153. package/dist/slack/socket-runner.d.ts +42 -0
  154. package/dist/slack/socket-runner.d.ts.map +1 -0
  155. package/dist/slack/thread-cache.d.ts +51 -0
  156. package/dist/slack/thread-cache.d.ts.map +1 -0
  157. package/dist/state-machine/context/build-engine-context.d.ts +8 -0
  158. package/dist/state-machine/context/build-engine-context.d.ts.map +1 -1
  159. package/dist/state-machine/dispatch/execution-invoker.d.ts.map +1 -1
  160. package/dist/state-machine/dispatch/foreach-processor.d.ts.map +1 -1
  161. package/dist/state-machine/dispatch/on-init-handlers.d.ts +43 -0
  162. package/dist/state-machine/dispatch/on-init-handlers.d.ts.map +1 -0
  163. package/dist/state-machine/dispatch/stats-manager.d.ts.map +1 -1
  164. package/dist/state-machine/dispatch/template-renderer.d.ts.map +1 -1
  165. package/dist/state-machine/runner.d.ts +6 -0
  166. package/dist/state-machine/runner.d.ts.map +1 -1
  167. package/dist/state-machine/states/level-dispatch.d.ts.map +1 -1
  168. package/dist/state-machine/states/plan-ready.d.ts.map +1 -1
  169. package/dist/state-machine/states/routing.d.ts.map +1 -1
  170. package/dist/state-machine/states/wave-planning.d.ts.map +1 -1
  171. package/dist/state-machine/workflow-projection.d.ts.map +1 -1
  172. package/dist/state-machine-execution-engine.d.ts +21 -9
  173. package/dist/state-machine-execution-engine.d.ts.map +1 -1
  174. package/dist/telemetry/state-capture.d.ts +5 -0
  175. package/dist/telemetry/state-capture.d.ts.map +1 -1
  176. package/dist/test-runner/core/flow-stage.d.ts.map +1 -1
  177. package/dist/test-runner/core/test-execution-wrapper.d.ts.map +1 -1
  178. package/dist/test-runner/evaluators.d.ts +37 -4
  179. package/dist/test-runner/evaluators.d.ts.map +1 -1
  180. package/dist/test-runner/index.d.ts +7 -0
  181. package/dist/test-runner/index.d.ts.map +1 -1
  182. package/dist/test-runner/recorders/slack-recorder.d.ts +17 -0
  183. package/dist/test-runner/recorders/slack-recorder.d.ts.map +1 -0
  184. package/dist/test-runner/validator.d.ts.map +1 -1
  185. package/dist/traces/{run-2025-11-21T11-50-46-505Z.ndjson → run-2026-01-21T05-37-24-446Z.ndjson} +91 -91
  186. package/dist/traces/run-2026-01-21T05-38-18-580Z.ndjson +1067 -0
  187. package/dist/types/bot.d.ts +109 -0
  188. package/dist/types/bot.d.ts.map +1 -0
  189. package/dist/types/cli.d.ts +4 -0
  190. package/dist/types/cli.d.ts.map +1 -1
  191. package/dist/types/config.d.ts +182 -5
  192. package/dist/types/config.d.ts.map +1 -1
  193. package/dist/types/engine.d.ts +5 -0
  194. package/dist/types/engine.d.ts.map +1 -1
  195. package/dist/types/git-checkout.d.ts +76 -0
  196. package/dist/types/git-checkout.d.ts.map +1 -0
  197. package/dist/utils/json-text-extractor.d.ts +17 -0
  198. package/dist/utils/json-text-extractor.d.ts.map +1 -0
  199. package/dist/utils/sandbox.d.ts +10 -0
  200. package/dist/utils/sandbox.d.ts.map +1 -1
  201. package/dist/utils/template-context.d.ts +1 -0
  202. package/dist/utils/template-context.d.ts.map +1 -1
  203. package/dist/utils/tracer-init.d.ts.map +1 -1
  204. package/dist/utils/workspace-manager.d.ts +118 -0
  205. package/dist/utils/workspace-manager.d.ts.map +1 -0
  206. package/dist/utils/worktree-cleanup.d.ts +33 -0
  207. package/dist/utils/worktree-cleanup.d.ts.map +1 -0
  208. package/dist/utils/worktree-manager.d.ts +153 -0
  209. package/dist/utils/worktree-manager.d.ts.map +1 -0
  210. package/dist/webhook-server.d.ts.map +1 -1
  211. package/dist/workflow-executor.d.ts.map +1 -1
  212. package/dist/workflow-registry.d.ts.map +1 -1
  213. package/package.json +5 -4
  214. package/dist/output/traces/run-2025-11-21T11-51-33-674Z.ndjson +0 -839
  215. package/dist/sdk/chunk-IEO6CFLG.mjs.map +0 -1
  216. package/dist/sdk/chunk-JEHPDJIF.mjs +0 -223
  217. package/dist/sdk/chunk-JEHPDJIF.mjs.map +0 -1
  218. package/dist/sdk/chunk-OZJ263FM.mjs.map +0 -1
  219. package/dist/sdk/chunk-PTL3K3PN.mjs.map +0 -1
  220. package/dist/sdk/chunk-VMPLF6FT.mjs.map +0 -1
  221. package/dist/sdk/github-frontend-4AWRJT7D.mjs.map +0 -1
  222. package/dist/sdk/host-7GBC3S7L.mjs.map +0 -1
  223. package/dist/sdk/memory-store-GJACZC2A.mjs +0 -11
  224. package/dist/sdk/workflow-registry-2YIIXQCK.mjs +0 -11
  225. package/dist/traces/run-2025-11-21T11-51-33-674Z.ndjson +0 -839
  226. /package/dist/sdk/{config-M4ZNO6NU.mjs.map → check-provider-registry-534KL5HT.mjs.map} +0 -0
  227. /package/dist/sdk/{chunk-37ZSCMFC.mjs.map → chunk-7UK3NIIT.mjs.map} +0 -0
  228. /package/dist/sdk/{chunk-6Y4YTKCF.mjs.map → chunk-NAW3DB3I.mjs.map} +0 -0
  229. /package/dist/sdk/{failure-condition-evaluator-NBO5YRXW.mjs.map → command-executor-TYUV6HUS.mjs.map} +0 -0
  230. /package/dist/sdk/{liquid-extensions-C7EG3YKH.mjs.map → config-YNC2EOOT.mjs.map} +0 -0
  231. /package/dist/sdk/{memory-store-GJACZC2A.mjs.map → failure-condition-evaluator-YGTF2GHG.mjs.map} +0 -0
  232. /package/dist/sdk/{routing-RP56JTV2.mjs.map → liquid-extensions-PKWCKK7E.mjs.map} +0 -0
  233. /package/dist/sdk/{session-registry-N5FFYFTM.mjs.map → memory-store-XGBB7LX7.mjs.map} +0 -0
  234. /package/dist/sdk/{tracer-init-WP4X46IF.mjs.map → prompt-state-YRJY6QAL.mjs.map} +0 -0
  235. /package/dist/sdk/{workflow-registry-2YIIXQCK.mjs.map → routing-6N45MJ4F.mjs.map} +0 -0
@@ -0,0 +1,192 @@
1
+ # RFC: Proper Pause/Resume for State-Machine Engine (Event‑Bus + Snapshots)
2
+
3
+ Status: draft
4
+
5
+ Owner: visor engine
6
+
7
+ Last updated: 2025-11-20
8
+
9
+ ## Summary
10
+
11
+ We will add first‑class pause/resume to the state‑machine engine so long‑running or interactive workflows (e.g., human‑input in Slack) can suspend execution and later continue exactly where they left off, without re‑running completed work. The design builds on our event‑bus architecture and the existing ExecutionJournal. It introduces JSON snapshots of the engine’s RunState and journal, and a resume entrypoint that hydrates a new runner from a snapshot. Slack and other frontends trigger resume when the awaited user event arrives.
12
+
13
+ ## Goals
14
+
15
+ - Pause a workflow at deterministic points (e.g., when HumanInputRequested is emitted) and resume later.
16
+ - Persist minimal, safe state; do not leak secrets.
17
+ - Avoid re‑executing completed checks; preserve outputs/history/routing.
18
+ - Keep the event‑driven integration pattern (no long‑lived in‑memory runs).
19
+ - Be robust to process restarts (snapshots on disk); work in CI and serverless.
20
+
21
+ ## Non‑Goals (initial)
22
+
23
+ - Arbitrary mid‑provider checkpointing (we pause at well‑defined integration points).
24
+ - Time‑travel debugging; only last consistent snapshot is kept by default.
25
+
26
+ ## Current State (as of this RFC)
27
+
28
+ - We already end the run on human‑input and rely on PromptState + a new event to re‑enter the workflow. This re‑entry currently performs a cold start and plans from scratch, which is acceptable but can be inefficient and can re‑visit guards.
29
+ - Engine has ExecutionJournal and experimental `saveSnapshotToFile()` that serializes RunState and journal; there is no hydrate/resume path.
30
+
31
+ ## High‑Level Design
32
+
33
+ 1) Snapshots (JSON)
34
+ - When the engine encounters a pause point (e.g., HumanInputRequested), it writes a snapshot JSON file containing:
35
+ - `version`: number
36
+ - `sessionId`, `event` (trigger), `wave`
37
+ - `state`: serialized RunState (via a new `serializeRunState()` + `deserializeRunState()` pair)
38
+ - `journal`: visible `JournalEntry[]` up to the snapshot
39
+ - `requestedChecks`: string[]
40
+ - `meta`: optional { checkId, channel, threadTs, threadKey, promptTs }
41
+
42
+ 2) Pause Points
43
+ - Initial scope: provider‑level pause during HumanInputRequested (event‑bus). Other future pause points can hook the same API.
44
+
45
+ 3) Resume Entry Point
46
+ - New `engine.resumeFromSnapshot(snapshot, overrides)` that:
47
+ - Rebuilds `EngineContext` (config, requestedChecks, sessionId, event, journal)
48
+ - Creates a new `StateMachineRunner`, calls `runner.setState(deserializedRunState)` (new API), and continues the main loop.
49
+ - Applies overrides such as `webhookContext` to allow providers to consume the awaited input.
50
+
51
+ 4) Frontends & PromptState
52
+ - Frontends (Slack) maintain the human prompt UI and store a pointer to the snapshot path in PromptState. On the awaited reply, frontends look up the snapshot and call `resumeFromSnapshot()` (via the socket/webhook path).
53
+
54
+ 5) Storage & Retention
55
+ - Default snapshot directory: `${VISOR_SNAPSHOT_DIR || '.visor/snapshots'}`.
56
+ - File naming: `${threadKey}-${checkId}.json` where `threadKey = "${channel}:${threadTs}"`.
57
+ - Retention: delete on successful resume; background TTL cleanup (e.g., 24h) to reap orphans.
58
+
59
+ ## Detailed Design
60
+
61
+ ### A. RunState serialization/hydration
62
+
63
+ - Today we have `serializeRunState(state)` for JSON. We will:
64
+ - Add `deserializeRunState(obj): RunState` (recreates Sets/Maps and ensures invariants).
65
+ - Add `StateMachineRunner.setState(state: RunState)`; only valid before `run()`; asserts state consistency.
66
+ - Ensure we never serialize provider internals or secrets; RunState contains only orchestration fields.
67
+
68
+ ### B. Engine APIs
69
+
70
+ - `StateMachineExecutionEngine.saveSnapshotToFile(filePath)` already exists.
71
+ - Add `resumeFromSnapshot(snapshot: SnapshotJson, opts?: { webhookContext?: ..., debug?: boolean }): Promise<ExecutionResult>`
72
+ - Recreate `EngineContext` using config and `snapshot.sessionId`.
73
+ - Rehydrate journal into a fresh `ExecutionJournal` (push `snapshot.journal`).
74
+ - Hydrate runner with `deserializeRunState(snapshot.state)` then continue.
75
+ - Wire `eventBus` and frontends like a normal run so integrations keep working.
76
+
77
+ ### C. Snapshot triggers & lifecycle
78
+
79
+ - Human‑input path:
80
+ - Provider emits `HumanInputRequested(checkId, prompt, channel, threadTs, threadKey)` and returns.
81
+ - Engine listens for `HumanInputRequested` during the run and immediately calls `saveSnapshotToFile()` to `${SNAP_DIR}/${threadKey}-${checkId}.json`.
82
+ - Slack Frontend posts the prompt, also sets PromptState for `${threadKey}` with `snapshotPath`.
83
+ - The run completes (no blocking).
84
+ - On Slack reply in the same thread, the socket path looks up `${snapshotPath}` and calls `resumeFromSnapshot()` with the current `webhookContext`.
85
+ - After a successful resume (terminal StateTransition), engine deletes the snapshot file and clears PromptState.
86
+
87
+ ### D. Idempotency & Safety
88
+
89
+ - Completed checks are recorded in RunState + journal; resuming does not re‑run them.
90
+ - We treat new inbound input as a new event; routing/guards continue from the hydrated state.
91
+ - If snapshot is missing or corrupted, we gracefully fall back to a cold run (today’s behavior).
92
+
93
+ ### E. Security/Privacy
94
+
95
+ - Snapshots omit secrets and environment variables. Only engine orchestration and committed results are stored.
96
+ - Snapshot directory is local by default; users can relocate via `VISOR_SNAPSHOT_DIR`.
97
+
98
+ ## File Layout
99
+
100
+ ```
101
+ .visor/
102
+ snapshots/
103
+ C123:1700.55-ask.json # example: threadKey+checkId
104
+ ```
105
+
106
+ ## Snapshot JSON (v1) — Example
107
+
108
+ ```json
109
+ {
110
+ "version": 1,
111
+ "sessionId": "a1b2c3",
112
+ "event": "issue_comment",
113
+ "wave": 2,
114
+ "state": { "currentState": "Routing", "wave": 2, "activeDispatches": [], "completedChecks": ["lint"], "stats": [], "historyLog": [], "forwardRunGuards": [], "currentLevelChecks": [], "pendingRunScopes": [] },
115
+ "journal": [
116
+ { "commitId": 1, "sessionId": "a1b2c3", "scope": [], "checkId": "lint", "event": "issue_comment", "result": { "issues": [] } }
117
+ ],
118
+ "requestedChecks": ["ask", "refine", "run-commands"],
119
+ "meta": { "checkId": "ask", "channel": "C123", "threadTs": "1700.55", "threadKey": "C123:1700.55", "promptTs": "1700.66" }
120
+ }
121
+ ```
122
+
123
+ ## Slack Integration Flow (pause/resume)
124
+
125
+ 1) Run emits `HumanInputRequested` → engine writes snapshot to `${threadKey}-${checkId}.json`.
126
+ 2) Slack Frontend posts prompt and sets PromptState with `snapshotPath`.
127
+ 3) User replies in same thread; socket receives the envelope, finds PromptState/snapshot.
128
+ 4) Engine `resumeFromSnapshot(snapshot, { webhookContext })` continues the workflow.
129
+ 5) On terminal state, snapshot is deleted and PromptState cleared.
130
+
131
+ ## CLI/Config
132
+
133
+ - Env:
134
+ - `VISOR_SNAPSHOT_DIR` — optional base directory for snapshots.
135
+ - Config (optional):
136
+ - `limits.max_workflow_depth` continues to apply; pause/resume doesn’t alter nesting rules.
137
+ - Future: `snapshots.enabled` (default true in Slack/webhook contexts), `snapshots.retentionHours`.
138
+
139
+ ## Failure Modes & Recovery
140
+
141
+ - Missing snapshot: fall back to cold run.
142
+ - Corrupt snapshot: log, fall back to cold run.
143
+ - Multiple prompts in the same thread: last snapshot wins; older ones are overwritten.
144
+ - Process restart: snapshots survive; PromptState TTL means we rely on snapshot presence to resume.
145
+
146
+ ## Testing Plan
147
+
148
+ 1) Unit
149
+ - `deserializeRunState(serializeRunState(state))` round‑trip equals for non‑object identity fields.
150
+ - `resumeFromSnapshot` continues and does not re‑execute completed checks (assert via journal size).
151
+
152
+ 2) Integration (Slack)
153
+ - First run → emits HumanInputRequested, snapshot written, prompt posted, run completes.
154
+ - Second run (reply) → loads snapshot, resumes, consumes reply, deletes snapshot.
155
+
156
+ 3) Crash/Restart Simulation
157
+ - Save snapshot, reset in‑memory state, then `resumeFromSnapshot` from file.
158
+
159
+ ## Rollout
160
+
161
+ - Phase 1 (behind feature switch in code): add hydrate APIs and write snapshots on HumanInputRequested; continue to cold‑run on reply but verify snapshot creation.
162
+ - Phase 2: wire socket path to call `resumeFromSnapshot`; delete snapshot on success.
163
+ - Phase 3: expand pause points if needed; add retention cleanup task.
164
+
165
+ ## Work Items
166
+
167
+ - Runner
168
+ - [ ] Add `deserializeRunState()`
169
+ - [ ] Add `runner.setState()`
170
+ - Engine
171
+ - [ ] Add `resumeFromSnapshot()`
172
+ - [ ] On HumanInputRequested → `saveSnapshotToFile()` (path via threadKey+checkId)
173
+ - Slack
174
+ - [ ] PromptState stores `snapshotPath` alongside prompt metadata
175
+ - [ ] Socket runner loads snapshot and calls `resumeFromSnapshot()` on reply
176
+ - Tests
177
+ - [ ] Unit: serialize/deserialize round‑trip
178
+ - [ ] Integration: pause/resume end‑to‑end (Slack fixture), snapshot deletion
179
+ - Docs
180
+ - [ ] Update Slack integration docs and human‑input provider docs
181
+
182
+ ## Alternatives Considered
183
+
184
+ - Keeping the runner alive across Slack replies → fragile in CI/serverless and ties up resources.
185
+ - Persisting only high‑level outputs and re‑planning on resume → simpler but can re‑emit side‑effects and re‑evaluate guards unexpectedly.
186
+
187
+ ## Open Questions
188
+
189
+ - Should we also emit a `RunPaused` event for analytics/observability?
190
+ - Do we want a structured `output.awaiting = true` signal at pause for downstream guards?
191
+ - Snapshot encryption at rest (out of scope for now; directory is local/trusted).
192
+
@@ -0,0 +1,253 @@
1
+ # Lifecycle Hooks
2
+
3
+ Lifecycle hooks allow you to execute preprocessing or setup tasks automatically before a step runs. This is particularly useful for enriching context, fetching external data, or preparing the environment.
4
+
5
+ ## `on_init` Hook
6
+
7
+ The `on_init` hook runs **before** a step executes, allowing you to:
8
+ - Fetch external data (JIRA issues, metrics, configuration)
9
+ - Enrich AI prompts with additional context
10
+ - Execute setup tasks or validation
11
+ - Invoke custom tools, steps, or workflows
12
+
13
+ ### Basic Usage
14
+
15
+ ```yaml
16
+ steps:
17
+ my-check:
18
+ type: ai
19
+ on_init:
20
+ run:
21
+ - tool: fetch-jira-issue
22
+ with:
23
+ issue_key: "PROJ-123"
24
+ as: jira-data
25
+ prompt: |
26
+ Review this PR considering JIRA issue: {{ outputs['jira-data'] | json }}
27
+ ```
28
+
29
+ ### Features
30
+
31
+ #### 1. Multiple Invocations
32
+
33
+ Execute multiple tools, steps, or workflows in sequence:
34
+
35
+ ```yaml
36
+ on_init:
37
+ run:
38
+ - tool: fetch-user-data
39
+ as: users
40
+ - tool: fetch-config
41
+ as: config
42
+ - workflow: validate-environment
43
+ as: validation
44
+ ```
45
+
46
+ #### 2. Custom Arguments
47
+
48
+ Pass arguments to tools and workflows using `with`:
49
+
50
+ ```yaml
51
+ on_init:
52
+ run:
53
+ - tool: fetch-external-data
54
+ with:
55
+ source: metrics-api
56
+ format: json
57
+ as: metrics
58
+ ```
59
+
60
+ #### 3. Custom Output Names
61
+
62
+ Store outputs with custom names using `as`:
63
+
64
+ ```yaml
65
+ on_init:
66
+ run:
67
+ - tool: fetch-jira-issue
68
+ with:
69
+ issue_key: "PROJ-456"
70
+ as: jira-context # Access via {{ outputs['jira-context'] }}
71
+ ```
72
+
73
+ #### 4. Dynamic Preprocessing
74
+
75
+ Use `run_js` for conditional preprocessing based on PR context:
76
+
77
+ ```yaml
78
+ on_init:
79
+ run_js: |
80
+ const items = [];
81
+
82
+ // Fetch JIRA only if PR title contains issue key
83
+ const jiraMatch = pr.title.match(/PROJ-\d+/);
84
+ if (jiraMatch) {
85
+ items.push({
86
+ tool: 'fetch-jira-issue',
87
+ with: { issue_key: jiraMatch[0] },
88
+ as: 'jira-data'
89
+ });
90
+ }
91
+
92
+ // Fetch metrics if backend files changed
93
+ if (files.some(f => f.filename.includes('backend/'))) {
94
+ items.push({
95
+ tool: 'fetch-metrics',
96
+ as: 'backend-metrics'
97
+ });
98
+ }
99
+
100
+ return items;
101
+ ```
102
+
103
+ ### Invocation Types
104
+
105
+ #### Tool Invocation
106
+
107
+ Execute a custom MCP tool:
108
+
109
+ ```yaml
110
+ on_init:
111
+ run:
112
+ - tool: my-custom-tool
113
+ with:
114
+ param1: value1
115
+ as: tool-output
116
+ ```
117
+
118
+ #### Step Invocation
119
+
120
+ Execute another step:
121
+
122
+ ```yaml
123
+ on_init:
124
+ run:
125
+ - step: preprocessing-step
126
+ with:
127
+ input: "{{ pr.title }}"
128
+ as: preprocessed
129
+ ```
130
+
131
+ #### Workflow Invocation
132
+
133
+ Execute a workflow:
134
+
135
+ ```yaml
136
+ on_init:
137
+ run:
138
+ - workflow: data-enrichment
139
+ with:
140
+ source: production
141
+ as: enriched-data
142
+ ```
143
+
144
+ ### Accessing Outputs
145
+
146
+ Outputs from `on_init` items are available in the step's execution context:
147
+
148
+ ```yaml
149
+ steps:
150
+ my-check:
151
+ type: command
152
+ on_init:
153
+ run:
154
+ - tool: fetch-data
155
+ as: external-data
156
+ exec: |
157
+ echo "Fetched data: {{ outputs['external-data'] | json }}"
158
+ ```
159
+
160
+ ### Reusable Tools and Workflows
161
+
162
+ Define tools and workflows in separate files and import them:
163
+
164
+ **reusable-tools.yaml:**
165
+ ```yaml
166
+ version: "1.0"
167
+ tools:
168
+ fetch-jira-issue:
169
+ name: fetch-jira-issue
170
+ exec: |
171
+ # Fetch JIRA issue...
172
+ parseJson: true
173
+ steps: {}
174
+ ```
175
+
176
+ **Main configuration:**
177
+ ```yaml
178
+ version: "1.0"
179
+ extends:
180
+ - ./reusable-tools.yaml
181
+
182
+ steps:
183
+ ai-review:
184
+ type: ai
185
+ on_init:
186
+ run:
187
+ - tool: fetch-jira-issue
188
+ with:
189
+ issue_key: "{{ pr.title | regex_search: '[A-Z]+-[0-9]+' }}"
190
+ as: jira
191
+ prompt: "Review considering: {{ outputs.jira | json }}"
192
+ ```
193
+
194
+ ### Loop Protection
195
+
196
+ To prevent infinite loops and excessive preprocessing:
197
+
198
+ - **Maximum 50 items**: `on_init` can execute at most 50 items (configurable via `MAX_ON_INIT_ITEMS`)
199
+ - **No nested execution**: `on_init` hooks within `on_init` items are skipped
200
+ - **Separate from routing loops**: `on_init` loop protection is independent of `on_success`/`on_fail` routing
201
+
202
+ ### forEach Integration
203
+
204
+ **How on_init works with forEach**: When a check uses `forEach`, the `on_init` hook runs **once before the forEach loop starts**, not once per item:
205
+
206
+ ```yaml
207
+ steps:
208
+ analyze-files:
209
+ type: ai
210
+ forEach: file-list
211
+ on_init: # Runs ONCE before processing all files
212
+ run:
213
+ - tool: fetch-project-config
214
+ as: config
215
+ prompt: |
216
+ Analyze {{ item }} using config: {{ outputs.config }}
217
+ # outputs.config is available to ALL forEach iterations
218
+ ```
219
+
220
+ This design allows you to:
221
+ - Fetch shared data once that all iterations can use
222
+ - Avoid redundant preprocessing for each item
223
+ - Keep forEach loops efficient
224
+
225
+ If you need per-item preprocessing, add `on_init` to child steps that depend on the forEach check.
226
+
227
+ ### Examples
228
+
229
+ See the `examples/` directory for comprehensive examples:
230
+
231
+ - **examples/reusable-tools.yaml** - Reusable tool library with 3 custom tools (fetch-jira-issue, fetch-external-data, validate-data)
232
+ - **examples/reusable-workflows.yaml** - Reusable workflow library with 3 workflows (data-enrichment, issue-triage, multi-step-validation)
233
+ - **examples/on-init-import-demo.yaml** - Complete demonstration showing:
234
+ - Using multiple imported tools in on_init
235
+ - Invoking imported workflows
236
+ - Chaining tools and workflows together
237
+ - Reusing the same tool multiple times with different parameters
238
+ - Includes 4 passing test cases
239
+
240
+ ### Best Practices
241
+
242
+ 1. **Keep preprocessing lightweight**: `on_init` runs before every step execution
243
+ 2. **Use custom output names**: Make outputs easy to identify with descriptive `as` names
244
+ 3. **Leverage reusability**: Define common tools/workflows once and import them
245
+ 4. **Use `run_js` for conditionals**: Avoid fetching unnecessary data
246
+ 5. **Handle failures gracefully**: Consider what happens if preprocessing fails
247
+
248
+ ### See Also
249
+
250
+ - [Custom Tools](./custom-tools.md) - Define reusable MCP tools
251
+ - [Workflows](./workflows.md) - Create reusable workflows
252
+ - [Liquid Templates](./liquid-templates.md) - Template syntax for dynamic values
253
+ - [RFC: on_init Hook](./rfc/on_init-hook.md) - Design proposal and rationale
@@ -198,8 +198,151 @@ echo '{{ pr | json }}' | jq .
198
198
  {{ files | map: "filename" }} # Array of filenames
199
199
  ```
200
200
 
201
+ ### Chat History Helper
202
+
203
+ The `chat_history` filter turns one or more check histories into a linear, timestamp‑sorted chat transcript. This is especially useful for human‑input + AI chat flows (Slack, CLI, etc.).
204
+
205
+ Basic usage:
206
+
207
+ ```liquid
208
+ {% assign history = '' | chat_history: 'ask', 'reply' %}
209
+ {% for m in history %}
210
+ {{ m.role | capitalize }}: {{ m.text }}
211
+ {% endfor %}
212
+ ```
213
+
214
+ Each `history` item has:
215
+
216
+ - `m.step` – originating check name (e.g. `"ask"`, `"reply"`)
217
+ - `m.role` – logical role (`"user"` or `"assistant"` by default)
218
+ - `m.text` – normalized text (from `.text` / `.content` / fallback field)
219
+ - `m.ts` – timestamp used for ordering
220
+ - `m.raw` – original `outputs_history[step][i]` object
221
+
222
+ By default:
223
+
224
+ - Human input checks (`type: human-input`) map to `role: "user"`.
225
+ - AI checks (`type: ai`) map to `role: "assistant"`.
226
+ - Messages are sorted by `ts` ascending across all steps.
227
+
228
+ Advanced options (all optional, passed as keyword arguments):
229
+
230
+ ```liquid
231
+ {% assign history = '' | chat_history:
232
+ 'ask',
233
+ 'reply',
234
+ direction: 'asc', # or 'desc' (default: 'asc')
235
+ limit: 50, # keep at most N messages (after sorting)
236
+ text: {
237
+ default_field: 'text', # which field to prefer when .text/.content missing
238
+ by_step: {
239
+ 'summary': 'summary.text' # use nested path for specific steps
240
+ }
241
+ },
242
+ roles: {
243
+ by_type: {
244
+ 'human-input': 'user',
245
+ 'ai': 'assistant'
246
+ },
247
+ by_step: {
248
+ 'system-note': 'system'
249
+ },
250
+ default: 'assistant'
251
+ },
252
+ role_map: 'ask=user,reply=assistant' # compact per-step override
253
+ %}
254
+ ```
255
+
256
+ Precedence for `role` resolution:
257
+
258
+ 1. `role_map` / `roles.by_step[step]` (explicit step override)
259
+ 2. `roles.by_type[checkType]` (e.g. `'human-input'`, `'ai'`)
260
+ 3. Built‑in defaults: `human-input → user`, `ai → assistant`
261
+ 4. `roles.default` if provided
262
+ 5. Fallback: `"assistant"`
263
+
264
+ Examples:
265
+
266
+ ```liquid
267
+ {%- assign history = '' | chat_history: 'ask', 'reply' -%}
268
+ {%- for m in history -%}
269
+ {{ m.role }}: {{ m.text }}
270
+ {%- endfor -%}
271
+ ```
272
+
273
+ ```liquid
274
+ {%- assign history = '' | chat_history: 'ask', 'clarify', 'reply', direction: 'desc', limit: 5 -%}
275
+ {%- for m in history -%}
276
+ [{{ m.step }}][{{ m.role }}] {{ m.text }}
277
+ {%- endfor -%}
278
+ ```
279
+
201
280
  <!-- Removed merge_sort_by example: filter no longer provided -->
202
281
 
282
+ ### Conversation Context (Slack, GitHub, etc.)
283
+
284
+ For transport‑aware prompts, Visor exposes a normalized `conversation` object in Liquid
285
+ for AI checks:
286
+
287
+ ```liquid
288
+ {% if conversation %}
289
+ Transport: {{ conversation.transport }} {# 'slack', 'github', ... #}
290
+ Thread: {{ conversation.thread.id }}
291
+ {% if conversation.thread.url %}
292
+ Link: {{ conversation.thread.url }}
293
+ {% endif %}
294
+
295
+ {% for m in conversation.messages %}
296
+ {{ m.user }} ({{ m.role }} at {{ m.timestamp }}): {{ m.text }}
297
+ {% endfor %}
298
+
299
+ Latest:
300
+ {{ conversation.current.user }} ({{ conversation.current.role }}): {{ conversation.current.text }}
301
+ {% endif %}
302
+ ```
303
+
304
+ Contract:
305
+
306
+ - `conversation.transport` – transport identifier (`'slack'`, `'github'`, etc.)
307
+ - `conversation.thread.id` – stable thread key:
308
+ - Slack: `"channel:thread_ts"`
309
+ - GitHub: `"owner/repo#number"`
310
+ - `conversation.thread.url` – optional deep link (Slack thread or GitHub PR/issue URL)
311
+ - `conversation.messages[]` – full history as normalized messages:
312
+ - `role`: `'user' | 'bot'`
313
+ - `user`: Slack user id or GitHub login
314
+ - `text`: message body
315
+ - `timestamp`: message timestamp
316
+ - `origin`: e.g. `'visor'` for bot messages, `'github'` for GitHub messages
317
+ - `conversation.current` – message that triggered the current run (same shape as above)
318
+ - `conversation.attributes` – extra metadata (e.g. `channel`, `thread_ts`, `owner`, `repo`, `number`, `event_name`, `action`)
319
+
320
+ Transport‑specific helpers:
321
+
322
+ - Slack:
323
+ - `slack.event` – raw Slack event payload (channel, ts, text, etc.)
324
+ - `slack.conversation` – same structure as `conversation` for Slack runs
325
+ - GitHub:
326
+ - `event` – GitHub event metadata and payload (unchanged)
327
+ - A normalized GitHub conversation is also attached so `conversation.transport == 'github'`
328
+ reflects the PR/issue body and comment history.
329
+
330
+ You can combine `conversation` with `chat_history`:
331
+
332
+ ```liquid
333
+ {% assign history = '' | chat_history: 'ask', 'reply' %}
334
+ {% if conversation and conversation.transport == 'slack' %}
335
+ # Slack thread:
336
+ {% for m in conversation.messages %}
337
+ {{ m.user }}: {{ m.text }}
338
+ {% endfor %}
339
+ {% endif %}
340
+
341
+ {% for m in history %}
342
+ [{{ m.step }}][{{ m.role }}] {{ m.text }}
343
+ {% endfor %}
344
+ ```
345
+
203
346
  ## Examples
204
347
 
205
348
  ### Debugging Outputs