openlanes 3.0.0 → 3.1.0

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 (329) hide show
  1. checksums.yaml +4 -4
  2. data/Brewfile +42 -0
  3. data/agents/bb8-resolver.md +49 -0
  4. data/agents/c3po-operator.md +62 -0
  5. data/agents/chewie-verifier.md +85 -0
  6. data/agents/hansolo-reviewer.md +20 -15
  7. data/agents/lando-broker.md +81 -19
  8. data/agents/leah-researcher.md +6 -6
  9. data/agents/luke-backend.md +41 -20
  10. data/agents/{palpatine-planner.md → luthen-rael-planner.md} +15 -13
  11. data/agents/palpatine-risk-assessor.md +112 -0
  12. data/agents/r2d2-mechanic.md +5 -1
  13. data/agents/rey-frontend.md +38 -19
  14. data/agents/yoda-writer.md +7 -7
  15. data/evals/cases/chewie-verifier/verify-medium/fail/plan/plan.md +7 -0
  16. data/evals/cases/chewie-verifier/verify-medium/fail/plan/pull-requests.md +10 -0
  17. data/evals/cases/chewie-verifier/verify-medium/fail/plan/spec.md +5 -0
  18. data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/final.png +0 -0
  19. data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/report.md +7 -0
  20. data/evals/cases/chewie-verifier/verify-medium/fail/repo/bin/greet +1 -0
  21. data/evals/cases/chewie-verifier/verify-medium/fail/run.json +1 -0
  22. data/evals/cases/chewie-verifier/verify-medium/pass/plan/plan.md +7 -0
  23. data/evals/cases/chewie-verifier/verify-medium/pass/plan/pull-requests.md +10 -0
  24. data/evals/cases/chewie-verifier/verify-medium/pass/plan/spec.md +5 -0
  25. data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/final.png +0 -0
  26. data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/report.md +14 -0
  27. data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/step-01.png +0 -0
  28. data/evals/cases/chewie-verifier/verify-medium/pass/repo/bin/greet +1 -0
  29. data/evals/cases/chewie-verifier/verify-medium/pass/run.json +1 -0
  30. data/evals/cases/chewie-verifier/verify-medium.yml +48 -0
  31. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/implementation-plan.md +5 -0
  32. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/plan.md +3 -0
  33. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/pull-requests.md +12 -0
  34. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/spec.md +5 -0
  35. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/lib/cart.rb +10 -0
  36. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/spec/cart_spec.rb +9 -0
  37. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/run.json +1 -0
  38. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/implementation-plan.md +5 -0
  39. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/plan.md +3 -0
  40. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/pull-requests.md +15 -0
  41. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/spec.md +5 -0
  42. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/lib/cart.rb +10 -0
  43. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/spec/cart_spec.rb +9 -0
  44. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/run.json +1 -0
  45. data/evals/cases/hansolo-reviewer/coverage-minimum.yml +76 -0
  46. data/evals/cases/luthen-rael-planner/plan-deep/fail/plan/plan.md +8 -0
  47. data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan.md +2 -2
  48. data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep.yml +1 -1
  49. data/evals/cases/luthen-rael-planner/plan-fast/fail/plan/plan.md +12 -0
  50. data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan.md +2 -2
  51. data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast.yml +1 -1
  52. data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/plan.md +20 -0
  53. data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/pull-requests.md +5 -0
  54. data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/spec.md +5 -0
  55. data/evals/cases/palpatine-risk-assessor/risk-medium/fail/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
  56. data/evals/cases/palpatine-risk-assessor/risk-medium/fail/run.json +1 -0
  57. data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/plan.md +25 -0
  58. data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/pull-requests.md +5 -0
  59. data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/spec.md +5 -0
  60. data/evals/cases/palpatine-risk-assessor/risk-medium/pass/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
  61. data/evals/cases/palpatine-risk-assessor/risk-medium/pass/run.json +1 -0
  62. data/evals/cases/palpatine-risk-assessor/risk-medium.yml +47 -0
  63. data/evals/cases/yoda-writer/spec-medium/pass/plan/spec.md +1 -1
  64. data/lib/openlanes/adapters/base.rb +24 -3
  65. data/lib/openlanes/adapters/capabilities.rb +29 -0
  66. data/lib/openlanes/adapters/claude.rb +32 -10
  67. data/lib/openlanes/adapters/codex.rb +9 -0
  68. data/lib/openlanes/adapters/pi.rb +10 -0
  69. data/lib/openlanes/adapters/stream.rb +145 -1
  70. data/lib/openlanes/adapters.rb +26 -2
  71. data/lib/openlanes/agents/agent.rb +4 -3
  72. data/lib/openlanes/agents/roster.rb +14 -2
  73. data/lib/openlanes/agents/routing.rb +43 -3
  74. data/lib/openlanes/apps/definition.rb +152 -0
  75. data/lib/openlanes/apps/framing.rb +114 -0
  76. data/lib/openlanes/apps/listing.rb +60 -0
  77. data/lib/openlanes/apps/ports.rb +168 -0
  78. data/lib/openlanes/apps/probe.rb +63 -0
  79. data/lib/openlanes/apps/process_spec.rb +39 -0
  80. data/lib/openlanes/apps/runner.rb +174 -0
  81. data/lib/openlanes/apps/status.rb +62 -0
  82. data/lib/openlanes/apps/supervisor.rb +277 -0
  83. data/lib/openlanes/apps/watcher.rb +50 -0
  84. data/lib/openlanes/apps.rb +41 -0
  85. data/lib/openlanes/cli/api/api.rb +10 -0
  86. data/lib/openlanes/cli/api/subcommands/docs.rb +63 -0
  87. data/lib/openlanes/cli/app/app.rb +31 -0
  88. data/lib/openlanes/cli/app/subcommands/start.rb +114 -0
  89. data/lib/openlanes/cli/app/subcommands/status.rb +38 -0
  90. data/lib/openlanes/cli/app/subcommands/stop.rb +44 -0
  91. data/lib/openlanes/cli/base.rb +89 -16
  92. data/lib/openlanes/cli/coverage/coverage.rb +68 -0
  93. data/lib/openlanes/cli/create/create.rb +11 -265
  94. data/lib/openlanes/cli/eval/eval.rb +4 -1
  95. data/lib/openlanes/cli/init/init.rb +39 -2
  96. data/lib/openlanes/cli/linear/subcommands/import.rb +1 -1
  97. data/lib/openlanes/cli/linear/subcommands/projects.rb +1 -1
  98. data/lib/openlanes/cli/list_plans/list_plans.rb +8 -2
  99. data/lib/openlanes/cli/mail/subcommands/ack.rb +52 -0
  100. data/lib/openlanes/cli/mail/subcommands/render.rb +31 -0
  101. data/lib/openlanes/cli/mail/subcommands/send.rb +26 -0
  102. data/lib/openlanes/cli/resync/subcommands/dirs.rb +53 -12
  103. data/lib/openlanes/cli/resync/subcommands/prs.rb +8 -69
  104. data/lib/openlanes/cli/review/review.rb +72 -0
  105. data/lib/openlanes/cli/run/run.rb +22 -454
  106. data/lib/openlanes/cli/setup/setup.rb +51 -0
  107. data/lib/openlanes/cli/start/start.rb +7 -64
  108. data/lib/openlanes/cli/state/state.rb +1 -1
  109. data/lib/openlanes/cli/state/subcommands/resume.rb +61 -0
  110. data/lib/openlanes/cli/state/subcommands/sign.rb +1 -1
  111. data/lib/openlanes/cli/unblock/unblock.rb +11 -205
  112. data/lib/openlanes/cli/web/web.rb +94 -8
  113. data/lib/openlanes/cli/worktree/worktree.rb +1 -1
  114. data/lib/openlanes/cli/worktree/worktree_prune.rb +77 -0
  115. data/lib/openlanes/cli.rb +37 -2
  116. data/lib/openlanes/coverage/cobertura.rb +49 -0
  117. data/lib/openlanes/coverage/config.rb +194 -0
  118. data/lib/openlanes/coverage/istanbul.rb +31 -0
  119. data/lib/openlanes/coverage/lcov.rb +38 -0
  120. data/lib/openlanes/coverage/report.rb +71 -0
  121. data/lib/openlanes/coverage/simplecov.rb +70 -0
  122. data/lib/openlanes/coverage.rb +33 -0
  123. data/lib/openlanes/engine/agent_locks.rb +231 -0
  124. data/lib/openlanes/engine/arbitration.rb +25 -0
  125. data/lib/openlanes/engine/board.rb +36 -5
  126. data/lib/openlanes/engine/conversions.rb +160 -0
  127. data/lib/openlanes/engine/coverage_gate.rb +217 -0
  128. data/lib/openlanes/engine/dispatcher.rb +1229 -116
  129. data/lib/openlanes/engine/event_journal/replay.rb +96 -0
  130. data/lib/openlanes/engine/event_journal.rb +148 -0
  131. data/lib/openlanes/engine/extensions.rb +271 -0
  132. data/lib/openlanes/engine/merging.rb +459 -0
  133. data/lib/openlanes/engine/narrator.rb +107 -0
  134. data/lib/openlanes/engine/plan_locks.rb +114 -0
  135. data/lib/openlanes/engine/pr_outcomes.rb +161 -0
  136. data/lib/openlanes/engine/pr_watch.rb +83 -0
  137. data/lib/openlanes/engine/progress_log.rb +38 -105
  138. data/lib/openlanes/engine/quality_question.rb +125 -0
  139. data/lib/openlanes/engine/rebase.rb +200 -0
  140. data/lib/openlanes/engine/run_workflow.rb +776 -0
  141. data/lib/openlanes/engine/runner.rb +186 -15
  142. data/lib/openlanes/engine/start_workflow.rb +77 -0
  143. data/lib/openlanes/engine/state_file.rb +101 -34
  144. data/lib/openlanes/engine/tally.rb +1 -1
  145. data/lib/openlanes/engine/verification/app_session.rb +104 -0
  146. data/lib/openlanes/engine/verification/driver.rb +314 -0
  147. data/lib/openlanes/engine/verification/outcome.rb +516 -0
  148. data/lib/openlanes/engine/verification/report.rb +95 -0
  149. data/lib/openlanes/engine/verification/round.rb +100 -0
  150. data/lib/openlanes/engine/verification/screenshots.rb +120 -0
  151. data/lib/openlanes/engine/verification.rb +57 -0
  152. data/lib/openlanes/engine.rb +30 -1
  153. data/lib/openlanes/execution/boundary.rb +212 -0
  154. data/lib/openlanes/execution/child.rb +4 -2
  155. data/lib/openlanes/execution/clock.rb +36 -6
  156. data/lib/openlanes/execution/control.rb +32 -0
  157. data/lib/openlanes/execution/executor.rb +114 -35
  158. data/lib/openlanes/execution/process_start.rb +63 -0
  159. data/lib/openlanes/execution/prompt.rb +236 -34
  160. data/lib/openlanes/execution/transcript.rb +159 -8
  161. data/lib/openlanes/execution.rb +21 -0
  162. data/lib/openlanes/lifecycle/adoption.rb +25 -8
  163. data/lib/openlanes/lifecycle/aging.rb +166 -0
  164. data/lib/openlanes/lifecycle/allocator.rb +216 -0
  165. data/lib/openlanes/lifecycle/brief.rb +32 -2
  166. data/lib/openlanes/lifecycle/completion.rb +12 -6
  167. data/lib/openlanes/lifecycle/create_workflow.rb +272 -0
  168. data/lib/openlanes/lifecycle/creator.rb +39 -11
  169. data/lib/openlanes/lifecycle/lanes.rb +1 -1
  170. data/lib/openlanes/lifecycle/resync.rb +170 -42
  171. data/lib/openlanes/lifecycle/unblock_workflow.rb +234 -0
  172. data/lib/openlanes/lifecycle/unblocker.rb +76 -2
  173. data/lib/openlanes/lifecycle.rb +37 -1
  174. data/lib/openlanes/linear/mapping.rb +4 -1
  175. data/lib/openlanes/linear/unit.rb +1 -1
  176. data/lib/openlanes/live/chat.rb +665 -0
  177. data/lib/openlanes/live/choice_converter.rb +166 -0
  178. data/lib/openlanes/live/choice_judge.rb +109 -0
  179. data/lib/openlanes/live/claims.rb +9 -1
  180. data/lib/openlanes/live/command.rb +13 -10
  181. data/lib/openlanes/live/commands.rb +221 -31
  182. data/lib/openlanes/live/copilot/brief.rb +128 -0
  183. data/lib/openlanes/live/copilot/cards.rb +226 -0
  184. data/lib/openlanes/live/copilot/operator.rb +259 -0
  185. data/lib/openlanes/live/copilot/slash.rb +82 -0
  186. data/lib/openlanes/live/copilot.rb +290 -0
  187. data/lib/openlanes/live/event_bus/base.rb +52 -0
  188. data/lib/openlanes/live/event_bus/memory_bus.rb +14 -0
  189. data/lib/openlanes/live/event_bus/redis_bus.rb +10 -0
  190. data/lib/openlanes/live/event_bus/transaction.rb +27 -0
  191. data/lib/openlanes/live/event_bus.rb +29 -0
  192. data/lib/openlanes/live/interviewer.rb +146 -0
  193. data/lib/openlanes/live/keys.rb +114 -0
  194. data/lib/openlanes/live/mail.rb +143 -0
  195. data/lib/openlanes/live/memory_store.rb +36 -1
  196. data/lib/openlanes/live/memory_subscriber.rb +40 -0
  197. data/lib/openlanes/live/news.rb +61 -0
  198. data/lib/openlanes/live/node.rb +20 -0
  199. data/lib/openlanes/live/outage.rb +225 -0
  200. data/lib/openlanes/live/outage_report.rb +89 -0
  201. data/lib/openlanes/live/publisher.rb +42 -2
  202. data/lib/openlanes/live/run_record.rb +39 -1
  203. data/lib/openlanes/live/run_state.rb +19 -2
  204. data/lib/openlanes/live/session.rb +38 -4
  205. data/lib/openlanes/live/snapshot.rb +103 -42
  206. data/lib/openlanes/live/stations.rb +12 -8
  207. data/lib/openlanes/live/store.rb +36 -1
  208. data/lib/openlanes/live/subscriber.rb +93 -0
  209. data/lib/openlanes/live.rb +71 -1
  210. data/lib/openlanes/plans/blocked_file.rb +117 -0
  211. data/lib/openlanes/plans/choices.rb +228 -0
  212. data/lib/openlanes/plans/dependencies.rb +304 -0
  213. data/lib/openlanes/plans/feature.rb +75 -16
  214. data/lib/openlanes/plans/journal.rb +169 -0
  215. data/lib/openlanes/plans/last_known_state.rb +82 -0
  216. data/lib/openlanes/plans/lock.rb +171 -0
  217. data/lib/openlanes/plans/mailbox.rb +145 -4
  218. data/lib/openlanes/plans/plan_state.rb +590 -9
  219. data/lib/openlanes/plans/pull_request_machine.rb +163 -0
  220. data/lib/openlanes/plans/review_label.rb +139 -0
  221. data/lib/openlanes/plans/spec.rb +239 -3
  222. data/lib/openlanes/plans/state_machine.rb +63 -30
  223. data/lib/openlanes/plans/status.rb +60 -11
  224. data/lib/openlanes/plans/subject.rb +4 -0
  225. data/lib/openlanes/plans/tree.rb +101 -10
  226. data/lib/openlanes/plans/verdict.rb +124 -0
  227. data/lib/openlanes/plans.rb +19 -0
  228. data/lib/openlanes/presentation/api_docs.rb +76 -0
  229. data/lib/openlanes/presentation/documentation.rb +105 -12
  230. data/lib/openlanes/presentation/pr_changes.rb +90 -0
  231. data/lib/openlanes/presentation/reporter.rb +66 -7
  232. data/lib/openlanes/presentation.rb +20 -1
  233. data/lib/openlanes/setup/check.rb +16 -0
  234. data/lib/openlanes/setup/machine.rb +179 -0
  235. data/lib/openlanes/setup.rb +34 -0
  236. data/lib/openlanes/support/clock.rb +57 -0
  237. data/lib/openlanes/support/config.rb +318 -6
  238. data/lib/openlanes/support/log.rb +206 -0
  239. data/lib/openlanes/support/refusal.rb +15 -0
  240. data/lib/openlanes/support/runtime.rb +61 -0
  241. data/lib/openlanes/support/spawner.rb +51 -0
  242. data/lib/openlanes/support/ui.rb +110 -21
  243. data/lib/openlanes/vcs/ci_gate.rb +31 -0
  244. data/lib/openlanes/vcs/github.rb +362 -12
  245. data/lib/openlanes/vcs/label_migration.rb +132 -0
  246. data/lib/openlanes/vcs/label_sync.rb +75 -0
  247. data/lib/openlanes/vcs/publisher.rb +287 -7
  248. data/lib/openlanes/vcs/refresh.rb +337 -0
  249. data/lib/openlanes/vcs/rename_date.rb +83 -0
  250. data/lib/openlanes/vcs/review.rb +66 -0
  251. data/lib/openlanes/vcs/worktree.rb +210 -14
  252. data/lib/openlanes/vcs.rb +18 -0
  253. data/lib/openlanes/version.rb +1 -1
  254. data/lib/openlanes/web/admin_auth.rb +104 -0
  255. data/lib/openlanes/web/app.rb +213 -15
  256. data/lib/openlanes/web/context.rb +33 -9
  257. data/lib/openlanes/web/demo.rb +39 -14
  258. data/lib/openlanes/web/demo_board.rb +63 -5
  259. data/lib/openlanes/web/events.rb +137 -9
  260. data/lib/openlanes/web/launcher.rb +70 -8
  261. data/lib/openlanes/web/listen.rb +133 -0
  262. data/lib/openlanes/web/plan_files.rb +136 -0
  263. data/lib/openlanes/web/request_log.rb +108 -0
  264. data/lib/openlanes/web/server.rb +31 -10
  265. data/lib/openlanes/web/stores.rb +11 -0
  266. data/lib/openlanes/web.rb +19 -4
  267. data/lib/openlanes.rb +45 -7
  268. data/openlanes.gemspec +9 -4
  269. data/schemas/journal-event.schema.json +72 -0
  270. data/schemas/plan-state.schema.json +163 -2
  271. data/skills/openlanes/SKILL.md +2 -2
  272. data/skills/openlanes-documents/SKILL.md +4 -4
  273. data/skills/openlanes-documents/references/blocked-template.md +2 -2
  274. data/skills/openlanes-documents/references/closing-templates.md +1 -1
  275. data/skills/openlanes-documents/references/plan-template.md +12 -4
  276. data/skills/openlanes-documents/references/retroactive-spec.md +1 -1
  277. data/skills/openlanes-run/SKILL.md +5 -5
  278. data/skills/openlanes-spec/SKILL.md +1 -1
  279. data/skills/openlanes-status/SKILL.md +1 -1
  280. data/web/dist/assets/favicon-BFJEKKAE.png +0 -0
  281. data/web/dist/assets/{index-Bp7W-ZTq.js → index-690ayLS4.js} +2 -2
  282. data/web/dist/assets/index-6ZmW4slI.js +199 -0
  283. data/web/dist/assets/{index-Byyxbh_z.js → index-B5KzlXp1.js} +2 -2
  284. data/web/dist/assets/{index-8dNhRmiO.js → index-BAo_v9bU.js} +2 -2
  285. data/web/dist/assets/{index-2xPC-eHt.js → index-BDm7g-xG.js} +2 -2
  286. data/web/dist/assets/{index-D0rmJN3h.js → index-BFH9bC7v.js} +2 -2
  287. data/web/dist/assets/{index-OR-odsl9.js → index-BVHPV7z4.js} +2 -2
  288. data/web/dist/assets/{index-C0VmkrO-.js → index-B_CPqjnA.js} +2 -2
  289. data/web/dist/assets/{index-DDI7zoAS.js → index-BhwBH5ct.js} +2 -2
  290. data/web/dist/assets/{index-BdR34cXC.js → index-Bq5AdwEt.js} +2 -2
  291. data/web/dist/assets/{index-uM88Njdj.js → index-CAW6ClM2.js} +2 -2
  292. data/web/dist/assets/index-Cp86u27u.css +1 -0
  293. data/web/dist/assets/{index-P3vSkiU-.js → index-Culm4pMO.js} +2 -2
  294. data/web/dist/assets/{index-DGLUuEXi.js → index-DElDJfp1.js} +2 -2
  295. data/web/dist/assets/{index-CYxcbpRM.js → index-Dja7osNW.js} +2 -2
  296. data/web/dist/assets/{index-popk9wbW.js → index-DjhU3FzU.js} +2 -2
  297. data/web/dist/assets/{index-mRV4XhJZ.js → index-Dutbxj4o.js} +2 -2
  298. data/web/dist/assets/{index-DB4QwuYn.js → index-RkDhPMDt.js} +2 -2
  299. data/web/dist/assets/{index-B7-O9ujQ.js → index-_mptZjlC.js} +2 -2
  300. data/web/dist/assets/logo-DXDCUiD8.png +0 -0
  301. data/web/dist/index.html +3 -3
  302. data/web/src/api/fixtures/board.json +203 -28
  303. metadata +1053 -881
  304. data/bin/factory-e2e +0 -157
  305. data/bin/plan-number +0 -164
  306. data/bin/setup +0 -26
  307. data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +0 -8
  308. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +0 -12
  309. data/web/dist/assets/favicon-C8pAuqqr.png +0 -0
  310. data/web/dist/assets/index-CI74IkWP.js +0 -198
  311. data/web/dist/assets/index-a4EHR-u4.css +0 -1
  312. data/web/dist/assets/logo-on-dark-DI9gCdN4.png +0 -0
  313. data/web/dist/assets/logo-on-light-BciwqxcJ.png +0 -0
  314. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/plan-backend.md +0 -0
  315. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/spec.md +0 -0
  316. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/run.json +0 -0
  317. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-backend.md +0 -0
  318. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-frontend.md +0 -0
  319. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/spec.md +0 -0
  320. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/run.json +0 -0
  321. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-backend.md +0 -0
  322. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-frontend.md +0 -0
  323. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/spec.md +0 -0
  324. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/run.json +0 -0
  325. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-backend.md +0 -0
  326. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-frontend.md +0 -0
  327. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/spec.md +0 -0
  328. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/run.json +0 -0
  329. /data/exe/{depot → ol} +0 -0
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/body_proxy"
4
+ require "rack/utils"
5
+
6
+ module OpenLanes
7
+ module Web
8
+ # One log entry per request, through {Log}, wrapped around the whole app
9
+ # so a request Rack::Protection or host authorization turns away is
10
+ # logged too.
11
+ #
12
+ # Not Sinatra's `logging` or Rack::CommonLogger: both write Apache-style
13
+ # text to `rack.errors`, which is the launcher's raw output file rather
14
+ # than the log the engine writes, and neither knows a page from a request.
15
+ #
16
+ # The event stream is the exception to "one entry per request". It stays
17
+ # open as long as the page does, so it is logged twice: when it opens, and
18
+ # when Puma closes its body, with how long it was open. Logged once, at
19
+ # the end, a page left open all day would not appear until evening.
20
+ #
21
+ # Static assets and `/healthz` are logged at debug: the launcher polls
22
+ # `/healthz` ten times a second while it waits, and a page load fetches a
23
+ # dozen assets, so at info they would bury the API calls.
24
+ class RequestLog
25
+ # A response of this type is a stream that outlives its request.
26
+ STREAM = "text/event-stream"
27
+
28
+ # Paths logged at info. Everything else is the page's own files.
29
+ API = %r{\A/api(/|\z)}
30
+
31
+ # @param app [#call]
32
+ # @param logger [#info, #debug, #error, nil] {Log.logger} when nil, read
33
+ # per request so a {Log.setup} after this was built still applies
34
+ # @param clock [#call] monotonic seconds
35
+ def initialize(app, logger: nil, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
36
+ @app = app
37
+ @logger = logger
38
+ @clock = clock
39
+ end
40
+
41
+ # @param env [Hash] the Rack environment
42
+ # @return [Array] status, headers and body, the body wrapped for a stream
43
+ def call(env)
44
+ started = @clock.call
45
+ status, headers, body = @app.call(env)
46
+ return [status, headers, opened(env, status, started, body)] if stream?(headers)
47
+
48
+ write(level(env), env, "#{line(env)} #{status}", status:, started:)
49
+ [status, headers, body]
50
+ rescue StandardError => e
51
+ write(:error, env, "#{line(env)} raised #{e.class}", status: 500, started:, exception: e)
52
+ raise
53
+ end
54
+
55
+ private
56
+
57
+ # Logs the stream opening, and hands back a body that logs its close.
58
+ #
59
+ # @return [Rack::BodyProxy]
60
+ def opened(env, status, started, body)
61
+ write(:info, env, "#{line(env)} stream opened", status:)
62
+ Rack::BodyProxy.new(body) { write(:info, env, "#{line(env)} stream closed", status:, started:) }
63
+ end
64
+
65
+ # @param headers [Hash]
66
+ # @return [Boolean]
67
+ def stream?(headers)
68
+ type = headers["content-type"] || headers["Content-Type"]
69
+ type.to_s.start_with?(STREAM)
70
+ end
71
+
72
+ # @param env [Hash]
73
+ # @return [Symbol]
74
+ def level(env) = env["PATH_INFO"].to_s.match?(API) ? :info : :debug
75
+
76
+ # @param env [Hash]
77
+ # @return [String] e.g. "GET /api/board"
78
+ def line(env) = "#{env["REQUEST_METHOD"]} #{env["PATH_INFO"]}"
79
+
80
+ # The page's own id, from `?page=`, so the requests one browser tab
81
+ # made can be picked out of several tabs' worth.
82
+ #
83
+ # @param env [Hash]
84
+ # @return [String, nil]
85
+ def page(env)
86
+ id = Rack::Utils.parse_query(env["QUERY_STRING"].to_s)["page"]
87
+ id.is_a?(String) && !id.empty? ? id : nil
88
+ end
89
+
90
+ # @param level [Symbol]
91
+ # @param env [Hash]
92
+ # @param message [String]
93
+ # @param status [Integer]
94
+ # @param started [Float, nil] when given, the entry carries the duration since
95
+ # @param exception [Exception, nil]
96
+ # @return [void]
97
+ def write(level, env, message, status:, started: nil, exception: nil)
98
+ payload = { method: env["REQUEST_METHOD"], path: env["PATH_INFO"], status: status.to_i, page: page(env) }.compact
99
+ entry = { message:, payload:, exception: }.compact
100
+ entry[:duration] = ((@clock.call - started) * 1000).round(1) if started
101
+ (@logger || Log.logger).public_send(level, **entry)
102
+ rescue StandardError
103
+ # A log that cannot be written is no reason to fail the request.
104
+ nil
105
+ end
106
+ end
107
+ end
108
+ end
@@ -7,7 +7,8 @@ require "puma/server"
7
7
  module OpenLanes
8
8
  module Web
9
9
  # The app on Puma, multi-threaded because every open page holds one
10
- # thread for its event stream, and bound to {HOST} only.
10
+ # thread for its event stream, and bound to {HOST} unless a {Listen}
11
+ # names other addresses.
11
12
  #
12
13
  # server = Web.server(app:, context:, port: 11000).start
13
14
  # server.url # => "http://127.0.0.1:11000"
@@ -18,29 +19,31 @@ module OpenLanes
18
19
  MAX_THREADS = 64
19
20
 
20
21
  # Seconds Puma waits for open streams after {#stop}. They notice the
21
- # context's `stopping` within a tick; this is the backstop.
22
+ # context's `stopping` on the `stop` nudge {#stop} publishes; this is
23
+ # the backstop, for a Redis that went away and took the nudge with it.
22
24
  SHUTDOWN_AFTER = 2
23
25
 
24
26
  # @param app [#call]
25
27
  # @param context [OpenLanes::Web::Context]
26
28
  # @param port [Integer] 0 for any free port
27
- # @param host [String]
29
+ # @param listen [OpenLanes::Web::Listen] the addresses to listen on
28
30
  # @param log [IO] where Puma writes its errors
29
- def initialize(app:, context:, port: DEFAULT_PORT, host: HOST, log: $stderr)
31
+ def initialize(app:, context:, port: DEFAULT_PORT, listen: Listen.parse(nil), log: $stderr)
30
32
  @app = app
31
33
  @context = context
32
34
  @port = Integer(port)
33
- @host = host
35
+ @listen = listen
34
36
  @log = log
35
37
  end
36
38
 
37
- # @return [String]
38
- attr_reader :host
39
+ # @return [OpenLanes::Web::Listen]
40
+ attr_reader :listen
39
41
 
40
42
  # Listen, and serve on Puma's own threads.
41
43
  #
42
44
  # @return [self]
43
45
  # @raise [Errno::EADDRINUSE] when the port is taken
46
+ # @raise [Errno::EADDRNOTAVAIL] when an address is not this machine's
44
47
  def start
45
48
  @puma = Puma::Server.new(@app,
46
49
  nil,
@@ -48,16 +51,29 @@ module OpenLanes
48
51
  max_threads: MAX_THREADS,
49
52
  force_shutdown_after: SHUTDOWN_AFTER,
50
53
  log_writer: Puma::LogWriter.new(File.open(File::NULL, "w"), @log))
51
- @puma.add_tcp_listener(@host, @port)
54
+ # Port 0 is resolved by the first listener, and the rest share it, so
55
+ # every address answers on the one port {#url} reports.
56
+ @listen.ips.each { @puma.add_tcp_listener(it, @puma.connected_ports.first || @port) }
52
57
  @thread = @puma.run
58
+ @context.chat&.start
53
59
  self
60
+ rescue SystemCallError
61
+ # A later address failing leaves the earlier ones bound; Puma is not
62
+ # running yet, so close its sockets here rather than leak them to
63
+ # whoever retries in this process.
64
+ @puma&.binder&.close
65
+ @puma = nil
66
+ raise
54
67
  end
55
68
 
56
69
  # @return [Integer] the port listened on, the real one when 0 was asked
57
70
  def port = @puma&.connected_ports&.first || @port
58
71
 
59
- # @return [String]
60
- def url = "http://#{@host}:#{port}"
72
+ # @return [String] where this machine's browser opens the page
73
+ def url = @listen.browse_url(port)
74
+
75
+ # @return [Array<String>] every address the page answers on
76
+ def urls = @listen.urls(port)
61
77
 
62
78
  # Block until the server stops.
63
79
  #
@@ -69,6 +85,11 @@ module OpenLanes
69
85
  # @return [void]
70
86
  def stop
71
87
  @context.stopping.make_true
88
+ # Wakes every stream blocked waiting for news, here and on any other
89
+ # server following this project, so ours see `stopping` now rather
90
+ # than at their next ping. The others look, find nothing, and sleep.
91
+ Live::News.publish(store: @context.store, keys: @context.keys, kind: "stop")
92
+ @context.chat&.stop
72
93
  @puma&.stop(true)
73
94
  @context.stores.close
74
95
  end
@@ -26,6 +26,17 @@ module OpenLanes
26
26
  # @return [OpenLanes::Live::Store, OpenLanes::Live::MemoryStore]
27
27
  def current = @mutex.synchronize { @by_thread[Thread.current] ||= @factory.call }
28
28
 
29
+ # Drop and close this thread's store. A thread that is not one of
30
+ # Puma's pooled ones calls it when done, or the map would keep a
31
+ # connection for every such thread that ever ran. A shared store is
32
+ # dropped but not closed: the others still hold it.
33
+ #
34
+ # @return [void]
35
+ def release
36
+ store = @mutex.synchronize { @by_thread.delete(Thread.current) }
37
+ store&.close unless @mutex.synchronize { @by_thread.value?(store) }
38
+ end
39
+
29
40
  # @return [void]
30
41
  def close
31
42
  stores = @mutex.synchronize { @by_thread.values.uniq.tap { @by_thread.clear } }
data/lib/openlanes/web.rb CHANGED
@@ -25,8 +25,8 @@ module OpenLanes
25
25
  # Where the server listens unless told otherwise (contract.md).
26
26
  DEFAULT_PORT = 11_000
27
27
 
28
- # Loopback only: the page can stop agents, and nothing off this machine
29
- # may reach it (W3).
28
+ # Loopback by default: the page can stop agents and has no login, so
29
+ # nothing off this machine reaches it unless `--ips` or `-I` says so (W3).
30
30
  HOST = "127.0.0.1"
31
31
 
32
32
  module_function
@@ -38,12 +38,27 @@ module OpenLanes
38
38
  # @param rest [Hash] {Context}'s other fields
39
39
  # @return [OpenLanes::Web::Context]
40
40
  def context(stores:, keys:, plans_dir:, url:, **rest)
41
+ rest[:chat] ||= chat(stores:, keys:, node: rest[:node])
41
42
  Context.new(stores:, keys:, plans_dir:, url:, csrf: SecureRandom.hex(32), **rest)
42
43
  end
43
44
 
45
+ # The chat between machines for one server (plan 006). Its machine is
46
+ # the node id in +keys+, so it answers for the same machine the board does.
47
+ #
48
+ # @param stores [OpenLanes::Web::Stores]
49
+ # @param keys [OpenLanes::Live::Keys]
50
+ # @param node [OpenLanes::Live::Node, nil]
51
+ # @return [OpenLanes::Live::Chat]
52
+ def chat(stores:, keys:, node: nil, **)
53
+ machine = keys.node.split(":").first
54
+ Live::Chat.new(stores:, keys:, machine:, label: node&.label || machine, **)
55
+ end
56
+
57
+ # The routes, with every request logged on the way out (see {RequestLog}).
58
+ #
44
59
  # @param context [OpenLanes::Web::Context]
45
- # @return [Class] a Rack application
46
- def app(context) = App.serving(context)
60
+ # @return [#call] a Rack application
61
+ def app(context) = RequestLog.new(App.serving(context))
47
62
 
48
63
  # @param app [#call]
49
64
  # @param context [OpenLanes::Web::Context]
data/lib/openlanes.rb CHANGED
@@ -6,7 +6,10 @@ require "dry/cli"
6
6
  require "dry/cli/help"
7
7
 
8
8
  require "dry/inflector"
9
+ # Every command workflow answers a Dry::Monads::Result (see OpenLanes::Refusal).
10
+ require "dry/monads"
9
11
  require "fileutils"
12
+ require "open3"
10
13
  require "shellwords"
11
14
  require "zeitwerk"
12
15
  loader = Zeitwerk::Loader.for_gem
@@ -21,13 +24,16 @@ loader.inflector.inflect(
21
24
  "ui" => "UI"
22
25
  )
23
26
  loader.collapse("#{__dir__}/openlanes/support")
27
+ loader.collapse("#{__dir__}/openlanes/cli/coverage")
24
28
  loader.collapse("#{__dir__}/openlanes/cli/create")
25
29
  loader.collapse("#{__dir__}/openlanes/cli/docs")
26
30
  loader.collapse("#{__dir__}/openlanes/cli/eval")
27
31
  loader.collapse("#{__dir__}/openlanes/cli/index")
28
32
  loader.collapse("#{__dir__}/openlanes/cli/init")
29
33
  loader.collapse("#{__dir__}/openlanes/cli/list_plans")
34
+ loader.collapse("#{__dir__}/openlanes/cli/review")
30
35
  loader.collapse("#{__dir__}/openlanes/cli/run")
36
+ loader.collapse("#{__dir__}/openlanes/cli/setup")
31
37
  loader.collapse("#{__dir__}/openlanes/cli/start")
32
38
  loader.collapse("#{__dir__}/openlanes/cli/states")
33
39
  loader.collapse("#{__dir__}/openlanes/cli/unblock")
@@ -40,9 +46,13 @@ loader.collapse("#{__dir__}/openlanes/cli/mail/subcommands")
40
46
  loader.collapse("#{__dir__}/openlanes/cli/state/subcommands")
41
47
  loader.collapse("#{__dir__}/openlanes/cli/resync/subcommands")
42
48
  loader.collapse("#{__dir__}/openlanes/cli/skills/subcommands")
49
+ loader.collapse("#{__dir__}/openlanes/cli/api/subcommands")
50
+ loader.ignore("#{__dir__}/openlanes/cli/api/api.rb")
51
+ loader.collapse("#{__dir__}/openlanes/cli/app/subcommands")
43
52
  loader.ignore("#{__dir__}/openlanes/cli/linear/linear.rb")
44
53
  loader.ignore("#{__dir__}/openlanes/cli/mail/mail.rb")
45
54
  loader.ignore("#{__dir__}/openlanes/cli/state/state.rb")
55
+ loader.ignore("#{__dir__}/openlanes/cli/app/app.rb")
46
56
  loader.ignore("#{__dir__}/openlanes/linear/mapping.rb")
47
57
  loader.setup
48
58
 
@@ -51,8 +61,8 @@ loader.setup
51
61
  # Three phases, each with a file that proves it happened, and a guarded state
52
62
  # machine that refuses to let a plan folder claim a phase it has not reached.
53
63
  #
54
- # spec ⚪️ New spec.md
55
- # plan ⭐️ Ready plan.md
64
+ # spec ⚪ New spec.md
65
+ # plan ⭐ Ready plan.md
56
66
  # build 🟡 → ✅ pull-requests.md
57
67
  #
58
68
  # The conventions are not documented anywhere else by hand: the status
@@ -95,6 +105,8 @@ module OpenLanes
95
105
  def self.move_directory(source, target)
96
106
  return false if File.exist?(target)
97
107
 
108
+ # Into archive/ and friends, which the first move there creates.
109
+ FileUtils.mkdir_p(File.dirname(target))
98
110
  parent = File.dirname(source)
99
111
  tracked = system("git",
100
112
  "-C",
@@ -116,7 +128,8 @@ module OpenLanes
116
128
  true
117
129
  end
118
130
 
119
- # Plan numbers visible in `.plans` on a git ref, without checking it out.
131
+ # Plan numbers visible in `.plans` on a git ref, without checking it out,
132
+ # the inactive subfolders included: an archived plan keeps its number.
120
133
  #
121
134
  # A pull request's branch is the only place that knows how far the sequence
122
135
  # had got when the work started, and branches are not all rebased onto the
@@ -127,14 +140,39 @@ module OpenLanes
127
140
  # @return [Array<OpenLanes::Plans::Ordinal>] possibly empty
128
141
  def self.plans_on_ref(root, ref)
129
142
  ["origin/#{ref}", ref].each do |candidate|
130
- out = `git -C #{root.shellescape} ls-tree -d --name-only #{candidate.shellescape} #{PLANS_DIR}/ 2>/dev/null`
131
- next if out.to_s.strip.empty?
132
-
133
- return out.lines.filter_map { |line| Plans::Ordinal.from_dirname(File.basename(line.strip)) }
143
+ found = plans_at(root, candidate)
144
+ return found unless found.empty?
134
145
  end
135
146
  []
136
147
  end
137
148
 
149
+ # Plan numbers in `.plans` on one tree-ish, the inactive subfolders
150
+ # included. {plans_on_ref} guesses at a branch name; this reads exactly
151
+ # what it is given, which is what allocating a number across every branch
152
+ # needs: a full ref name tried as `origin/refs/heads/x` would cost a
153
+ # second `ls-tree` per branch for nothing.
154
+ #
155
+ # @param root [String] any directory inside the repository
156
+ # @param treeish [String] a ref, or a commit
157
+ # @param plans [String] the `.plans` directory, relative to +root+
158
+ # @return [Array<OpenLanes::Plans::Ordinal>] empty when the ref is unknown
159
+ # or carries no plans
160
+ def self.plans_at(root, treeish, plans: PLANS_DIR)
161
+ dirs = [plans, *Plans::Tree::SUBFOLDERS.map { "#{plans}/#{it}" }].map { "#{it}/" }
162
+ out, status = Open3.capture2("git",
163
+ "-C",
164
+ root,
165
+ "ls-tree",
166
+ "-d",
167
+ "--name-only",
168
+ treeish,
169
+ *dirs,
170
+ err: File::NULL)
171
+ return [] unless status.success?
172
+
173
+ out.lines.filter_map { |line| Plans::Ordinal.from_dirname(File.basename(line.strip)) }
174
+ end
175
+
138
176
  # The agent that writes a specification for work that already shipped.
139
177
  # Named here rather than in the CLI so the definition file stays the single
140
178
  # source of truth about who does what.
data/openlanes.gemspec CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  TEXT
18
18
  spec.homepage = "https://github.com/kigster/openlanes"
19
19
 
20
- spec.required_ruby_version = ">= 3" # rubocop:disable Gemspec/RequiredRubyVersion -- the floor users get, not the Ruby the cops lint for
20
+ spec.required_ruby_version = ">= 4" # rubocop:disable Gemspec/RequiredRubyVersion -- the floor users get, not the Ruby the cops lint for
21
21
 
22
22
  spec.metadata = {
23
23
  "homepage_uri" => spec.homepage,
@@ -33,7 +33,7 @@ Gem::Specification.new do |spec|
33
33
  # `openlanes web --demo` replays.
34
34
  # Source maps stay out: they double the page's size for nobody who installs it.
35
35
  globbed = Dir.glob(["*.gemspec", "lib/**/*.rb", "agents/*.md", "schemas/*.json", "evals/cases/**/*", "skills/**/*",
36
- "exe/*", "bin/*", "web/dist/**/*", "web/src/api/fixtures/board.json"])
36
+ "exe/*", "web/dist/**/*", "web/src/api/fixtures/board.json", "Brewfile"])
37
37
  globbed.select { |f| File.file?(f) && !f.end_with?(".map") }
38
38
  end
39
39
 
@@ -49,10 +49,10 @@ Gem::Specification.new do |spec|
49
49
  end
50
50
 
51
51
  spec.bindir = "exe"
52
- # `depot` and `lanes` are symlinks to `openlanes` in the checkout; named here
52
+ # `ol` and `lanes` are symlinks to `openlanes` in the checkout; named here
53
53
  # so that installing the gem writes all three, rather than only the one git
54
54
  # recorded.
55
- spec.executables = %w[openlanes depot lanes]
55
+ spec.executables = %w[openlanes ol lanes]
56
56
  spec.require_paths = ["lib"]
57
57
 
58
58
  spec.add_dependency "aasm"
@@ -66,6 +66,7 @@ Gem::Specification.new do |spec|
66
66
  spec.add_dependency "dry-inflector"
67
67
  spec.add_dependency "dry-monads"
68
68
  spec.add_dependency "fuzzy-string-match"
69
+ spec.add_dependency "inquirex-llm", "~> 0.12"
69
70
  # Opens the control room in the browser when `openlanes run` starts the API server.
70
71
  spec.add_dependency "launchy", "~> 3.1"
71
72
  spec.add_dependency "parallel"
@@ -76,7 +77,9 @@ Gem::Specification.new do |spec|
76
77
  spec.add_dependency "rackup", "~> 2.1"
77
78
  # The live run (plan 004) lives in Redis: board and command streams, plan claims.
78
79
  spec.add_dependency "redis-client", "~> 0.31"
80
+ spec.add_dependency "semantic_logger", "~> 5.1"
79
81
  spec.add_dependency "sinatra", "~> 4.2"
82
+ spec.add_dependency "agent-lock"
80
83
  spec.add_dependency "tty-box"
81
84
  spec.add_dependency "tty-command"
82
85
  spec.add_dependency "tty-cursor"
@@ -84,4 +87,6 @@ Gem::Specification.new do |spec|
84
87
  spec.add_dependency "tty-screen"
85
88
  spec.add_dependency "unicode-display_width"
86
89
  spec.add_dependency "zeitwerk"
90
+ spec.add_dependency "newrelic_rpm"
91
+ spec.add_dependency "newrelic-infinite_tracing"
87
92
  end
@@ -0,0 +1,72 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/kigster/openlanes/schemas/journal-event.schema.json",
4
+ "title": "openlanes engine journal event",
5
+ "description": "One line of a node's NDJSON progress journal, written by OpenLanes::Engine::EventJournal under Runtime.journal_dir. Append-only, fsync'd per event.",
6
+ "type": "object",
7
+ "required": ["v", "seq", "at", "node", "run_id", "kind"],
8
+ "properties": {
9
+ "v": { "const": 1 },
10
+ "seq": { "type": "integer", "minimum": 1 },
11
+ "at": { "type": "string", "format": "date-time" },
12
+ "node": { "type": "string", "minLength": 1 },
13
+ "run_id": { "type": "string", "minLength": 1 },
14
+ "kind": {
15
+ "enum": [
16
+ "run_started", "run_ended", "plan_assigned", "agent_started", "agent_session",
17
+ "agent_offset", "agent_ended", "transition", "extension", "question"
18
+ ]
19
+ },
20
+ "plan": { "type": "string", "pattern": "^\\d{3}\\.\\d{2}$" },
21
+ "agent": { "type": "string", "minLength": 1 },
22
+ "round": { "type": "integer", "minimum": 0 },
23
+ "pid": { "type": "integer", "minimum": 1 },
24
+ "started_at": { "type": ["string", "null"], "format": "date-time" },
25
+ "session_id": { "type": ["string", "null"] },
26
+ "output_file": { "type": ["string", "null"] },
27
+ "offset": { "type": "integer", "minimum": 0 },
28
+ "exit": { "type": ["integer", "null"] },
29
+ "from": { "type": "string" },
30
+ "to": { "type": "string" },
31
+ "event": { "type": "string" },
32
+ "seconds": { "type": "number", "minimum": 0 },
33
+ "note": { "type": "string" },
34
+ "reason": { "type": "string" },
35
+ "jobs": { "type": "integer", "minimum": 1 },
36
+ "isolation": { "type": "string" }
37
+ },
38
+ "allOf": [
39
+ {
40
+ "if": { "properties": { "kind": { "const": "plan_assigned" } } },
41
+ "then": { "required": ["plan", "agent"] }
42
+ },
43
+ {
44
+ "if": { "properties": { "kind": { "const": "agent_started" } } },
45
+ "then": { "required": ["plan", "agent", "pid", "started_at", "session_id", "output_file"] }
46
+ },
47
+ {
48
+ "if": { "properties": { "kind": { "const": "agent_session" } } },
49
+ "then": { "required": ["plan", "agent", "session_id"], "properties": { "session_id": { "type": "string" } } }
50
+ },
51
+ {
52
+ "if": { "properties": { "kind": { "const": "agent_offset" } } },
53
+ "then": { "required": ["plan", "agent", "offset"] }
54
+ },
55
+ {
56
+ "if": { "properties": { "kind": { "const": "agent_ended" } } },
57
+ "then": { "required": ["plan", "agent", "exit"] }
58
+ },
59
+ {
60
+ "if": { "properties": { "kind": { "const": "transition" } } },
61
+ "then": { "required": ["plan", "from", "to"] }
62
+ },
63
+ {
64
+ "if": { "properties": { "kind": { "const": "extension" } } },
65
+ "then": { "required": ["plan", "agent", "seconds"] }
66
+ },
67
+ {
68
+ "if": { "properties": { "kind": { "const": "question" } } },
69
+ "then": { "required": ["plan", "agent", "note"] }
70
+ }
71
+ ]
72
+ }