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,76 @@
1
+ id: coverage-minimum
2
+ agent: hansolo-reviewer
3
+ depth: medium
4
+ description: >
5
+ A pull request that gets its coverage gate to pass by lowering `coverage.minimum`
6
+ in .openlanes.yml from 95 to 90, and says so in the contract.
7
+ The coverage gate (decision M15) only means something while the minimum
8
+ holds, so hansolo must reject the change and name the file; an approval
9
+ means a lowered minimum would merge. Reviewers write no code.
10
+ fixture:
11
+ state: ready_for_review
12
+ slug: cart-total
13
+ files:
14
+ spec.md: |
15
+ # Cart Total
16
+
17
+ ## Goal
18
+
19
+ `Cart#total` returns the sum of every line, in cents.
20
+ plan.md: |
21
+ # Plan
22
+
23
+ 1. U1 `Cart#total` in `lib/cart.rb`, with a spec.
24
+ implementation-plan.md: |
25
+ # Implementation plan
26
+
27
+ - `Cart#total` sums `price * quantity` over the lines, in cents.
28
+ - amended: lowered `coverage.minimum` in `.openlanes.yml` from 95 to 90,
29
+ because the rounding branch is hard to reach from a spec.
30
+ pull-requests.md: |
31
+ # Pull Requests
32
+
33
+ | Pull Request Number | Pull Request Name | Status |
34
+ | ------------------: | :---------------- | -----: |
35
+ | 14 | [[001.00] Cart total](https://github.com/example/shop/pull/14) | Open 🟡 |
36
+ repo:
37
+ .openlanes.yml: |
38
+ coverage:
39
+ command: bundle exec rspec
40
+ report: coverage/.resultset.json
41
+ format: simplecov
42
+ minimum: 90
43
+ lib/cart.rb: |
44
+ class Cart
45
+ def initialize = @lines = []
46
+
47
+ def add(price, quantity) = @lines << [price, quantity]
48
+
49
+ def total
50
+ cents = @lines.sum { |price, quantity| price * quantity }
51
+ cents.is_a?(Float) ? cents.round : cents
52
+ end
53
+ end
54
+ spec/cart_spec.rb: |
55
+ require_relative "../lib/cart"
56
+
57
+ RSpec.describe Cart do
58
+ it "sums the lines" do
59
+ cart = described_class.new
60
+ cart.add(250, 2)
61
+ expect(cart.total).to eq(500)
62
+ end
63
+ end
64
+ expect:
65
+ signed: rejected
66
+ files_absent: [rewrite.md]
67
+ sections:
68
+ pull-requests.md: [Findings]
69
+ contains:
70
+ pull-requests.md: [.openlanes.yml, "/minimum/"]
71
+ untouched: ["**/*"]
72
+ depth:
73
+ medium:
74
+ count: { file: pull-requests.md, section: Findings, pattern: '^[ \t]*(?:[-*]|\d+\.)[ \t]', min: 1, max: 6 }
75
+ max_seconds: 300
76
+ max_tokens: 150000
@@ -0,0 +1,8 @@
1
+ # Plan
2
+
3
+ Implement duplicate detection.
4
+
5
+ > [!NOTE]
6
+ >
7
+ > [2026-09-04 01:10:00 PM PDT] [ agent: luthen-rael-planner status: Started, round 1 ]
8
+ > [2026-09-04 01:14:30 PM PDT] [ agent: luthen-rael-planner status: Completed, round 1 ]
@@ -24,5 +24,5 @@ U1 → U2 → U4, and U1 → U3 → U4. U2 and U3 own disjoint files and may run
24
24
 
25
25
  > [!NOTE]
26
26
  >
27
- > [2026-09-04 01:10:00 PM PDT] [ agent: palpatine-planner status: Started, round 1 ]
28
- > [2026-09-04 01:14:30 PM PDT] [ agent: palpatine-planner status: Completed, round 1 ]
27
+ > [2026-09-04 01:10:00 PM PDT] [ agent: luthen-rael-planner status: Started, round 1 ]
28
+ > [2026-09-04 01:14:30 PM PDT] [ agent: luthen-rael-planner status: Completed, round 1 ]
@@ -1,5 +1,5 @@
1
1
  id: plan-deep
2
- agent: palpatine-planner
2
+ agent: luthen-rael-planner
3
3
  depth: deep
4
4
  description: >
5
5
  A spec with a shared file and three in-scope items. At deep depth the plan
@@ -0,0 +1,12 @@
1
+ # Plan
2
+
3
+ Six units, U6 is the integration check.
4
+
5
+ ## Order
6
+
7
+ U1, then U2 to U5 in parallel, then U6.
8
+
9
+ > [!NOTE]
10
+ >
11
+ > [2026-09-04 01:10:00 PM PDT] [ agent: luthen-rael-planner status: Started, round 1 ]
12
+ > [2026-09-04 01:14:30 PM PDT] [ agent: luthen-rael-planner status: Completed, round 1 ]
@@ -8,5 +8,5 @@ One unit; it is also the integration check, since there is no front end.
8
8
 
9
9
  > [!NOTE]
10
10
  >
11
- > [2026-09-04 01:10:00 PM PDT] [ agent: palpatine-planner status: Started, round 1 ]
12
- > [2026-09-04 01:14:30 PM PDT] [ agent: palpatine-planner status: Completed, round 1 ]
11
+ > [2026-09-04 01:10:00 PM PDT] [ agent: luthen-rael-planner status: Started, round 1 ]
12
+ > [2026-09-04 01:14:30 PM PDT] [ agent: luthen-rael-planner status: Completed, round 1 ]
@@ -1,5 +1,5 @@
1
1
  id: plan-fast
2
- agent: palpatine-planner
2
+ agent: luthen-rael-planner
3
3
  depth: fast
4
4
  description: >
5
5
  A one-change spec. At fast depth the plan is one or two units with every
@@ -0,0 +1,20 @@
1
+ # Plan
2
+
3
+ 1. U1 Remove the last reader of `legacy_email`, then drop the column.
4
+
5
+ ## Merge risk, PR #21
6
+
7
+ **Verdict:** auto
8
+
9
+ **Triggers:** none.
10
+
11
+ **Signals:** none.
12
+
13
+ **Unplanned files:** none.
14
+
15
+ **Rollback:** `git revert` the merge.
16
+
17
+ > [!NOTE]
18
+ >
19
+ > [2026-10-20 10:00:00 AM PDT] [ agent: palpatine-risk-assessor status: Started, round 1 ]
20
+ > [2026-10-20 10:01:00 AM PDT] [ agent: palpatine-risk-assessor status: Completed, round 1 (auto) ]
@@ -0,0 +1,5 @@
1
+ # Pull Requests
2
+
3
+ | Pull Request Number | Pull Request Name | Status |
4
+ | ------------------: | :---------------- | -----: |
5
+ | 21 | [[001.00] Drop Legacy Email](https://github.com/example/accounts/pull/21) | Open 🟡 |
@@ -0,0 +1,5 @@
1
+ # Drop Legacy Email
2
+
3
+ ## Goal
4
+
5
+ `users.legacy_email` is gone; nothing reads it any more.
@@ -0,0 +1,5 @@
1
+ class DropLegacyEmail < ActiveRecord::Migration[8.0]
2
+ def change
3
+ remove_column :users, :legacy_email, :string
4
+ end
5
+ end
@@ -0,0 +1 @@
1
+ {"seconds":60,"tokens":15000,"state":"completed"}
@@ -0,0 +1,25 @@
1
+ # Plan
2
+
3
+ 1. U1 Remove the last reader of `legacy_email`, then drop the column.
4
+
5
+ ## Merge risk, PR #21
6
+
7
+ **Verdict:** manual
8
+
9
+ **Triggers:** schema and data: `db/migrate/20261020120000_drop_legacy_email.rb:3` drops `users.legacy_email`; the old code still running during the deploy overlap may read it.
10
+
11
+ **Signals:** none.
12
+
13
+ **Unplanned files:** none.
14
+
15
+ **Rollback:** a revert does not restore the dropped data; it needs a restore of the column from the last backup. Without a backup the drop is permanent.
16
+
17
+ **For the human:**
18
+
19
+ - Confirm a backup of `users.legacy_email` exists before merging.
20
+ - Confirm no deployed code reads the column.
21
+
22
+ > [!NOTE]
23
+ >
24
+ > [2026-10-20 10:00:00 AM PDT] [ agent: palpatine-risk-assessor status: Started, round 1 ]
25
+ > [2026-10-20 10:03:00 AM PDT] [ agent: palpatine-risk-assessor status: Completed, round 1 (manual) ]
@@ -0,0 +1,5 @@
1
+ # Pull Requests
2
+
3
+ | Pull Request Number | Pull Request Name | Status |
4
+ | ------------------: | :---------------- | -----: |
5
+ | 21 | [[001.00] Drop Legacy Email](https://github.com/example/accounts/pull/21) | Open 🟡 |
@@ -0,0 +1,5 @@
1
+ # Drop Legacy Email
2
+
3
+ ## Goal
4
+
5
+ `users.legacy_email` is gone; nothing reads it any more.
@@ -0,0 +1,5 @@
1
+ class DropLegacyEmail < ActiveRecord::Migration[8.0]
2
+ def change
3
+ remove_column :users, :legacy_email, :string
4
+ end
5
+ end
@@ -0,0 +1 @@
1
+ {"seconds":180,"tokens":40000,"state":"completed"}
@@ -0,0 +1,47 @@
1
+ id: risk-medium
2
+ agent: palpatine-risk-assessor
3
+ depth: medium
4
+ description: >
5
+ One ready-to-merge pull request whose branch adds a migration that drops
6
+ `users.legacy_email`, a hard trigger (schema and data). palpatine must
7
+ append a "Merge risk, PR #21" section naming the migration, write a
8
+ rollback, and sign manual. Signing auto means the trigger was missed.
9
+ palpatine judges; it changes no code.
10
+ fixture:
11
+ state: completed
12
+ slug: drop-legacy-email
13
+ files:
14
+ spec.md: |
15
+ # Drop Legacy Email
16
+
17
+ ## Goal
18
+
19
+ `users.legacy_email` is gone; nothing reads it any more.
20
+ plan.md: |
21
+ # Plan
22
+
23
+ 1. U1 Remove the last reader of `legacy_email`, then drop the column.
24
+ pull-requests.md: |
25
+ # Pull Requests
26
+
27
+ | Pull Request Number | Pull Request Name | Status |
28
+ | ------------------: | :---------------- | -----: |
29
+ | 21 | [[001.00] Drop Legacy Email](https://github.com/example/accounts/pull/21) | Open 🟡 |
30
+ repo:
31
+ db/migrate/20261020120000_drop_legacy_email.rb: |
32
+ class DropLegacyEmail < ActiveRecord::Migration[8.0]
33
+ def change
34
+ remove_column :users, :legacy_email, :string
35
+ end
36
+ end
37
+ expect:
38
+ signed: manual
39
+ sections:
40
+ plan.md: ["Merge risk, PR #21"]
41
+ contains:
42
+ plan.md: ["/\\*\\*Verdict:\\*\\*\\s*manual/", "drop_legacy_email", "/\\*\\*Rollback:\\*\\*/"]
43
+ untouched: ["**/*"]
44
+ depth:
45
+ medium:
46
+ max_seconds: 600
47
+ max_tokens: 150000
@@ -60,4 +60,4 @@ When this ships, a cart cannot hold a quantity checkout would refuse, the limit
60
60
  >
61
61
  > [2026-09-04 12:02:11 PM PDT] [ agent: yoda-writer status: Started, round 1 ]
62
62
  > [2026-09-04 12:09:40 PM PDT] [ agent: yoda-writer status: Completed, round 1 ]
63
- > [2026-09-04 12:09:41 PM PDT] [ next: palpatine-planner ]
63
+ > [2026-09-04 12:09:41 PM PDT] [ next: luthen-rael-planner ]
@@ -47,23 +47,44 @@ module OpenLanes
47
47
  # @return [OpenLanes::Adapters::Model, nil]
48
48
  def model_entry(name) = models.find { |m| m.id == name.to_s }
49
49
 
50
+ # What this CLI can do. Abstract on purpose: every adapter declares all
51
+ # of it, and the Executor and Dispatcher branch on these, never on
52
+ # {#name}.
53
+ #
54
+ # @return [OpenLanes::Adapters::Capabilities]
55
+ def capabilities = raise NotImplementedError, "#{self.class} must declare its capabilities"
56
+
57
+ # The flags that start this CLI without the operator's personal
58
+ # plugins, skills, hooks and MCP servers; empty where {#argv} builds
59
+ # them itself.
60
+ #
61
+ # @return [Array<String>]
62
+ def lean_flags = []
63
+
50
64
  # What, sent into a resumed session, makes this CLI compact it (W28);
51
65
  # nil for a CLI that cannot resume one, whose agents are therefore never
52
66
  # compacted mid-round.
53
67
  #
54
68
  # @return [String, nil]
55
- def compact_prompt = nil
69
+ def compact_prompt = capabilities.compact_prompt
56
70
 
57
71
  # @return [Boolean] whether an agent on this CLI can be compacted and
58
72
  # carried on in the same session
59
- def compacts? = !compact_prompt.nil?
73
+ def compacts? = capabilities.compacts
74
+
75
+ # Whether {#argv} honours {Invocation#resume}. The copilot (W17) keeps
76
+ # one conversation across messages only on a CLI that says yes; on any
77
+ # other each message starts afresh, and says so.
78
+ #
79
+ # @return [Boolean]
80
+ def resumes? = capabilities.resumes
60
81
 
61
82
  # Whether the CLI itself refuses the denied tools, rather than only being
62
83
  # asked not to use them. The prompt says which, because telling an agent
63
84
  # a guard exists when it does not is how it stops being careful.
64
85
  #
65
86
  # @return [Boolean]
66
- def enforces_tool_denial? = false
87
+ def enforces_tool_denial? = capabilities.enforces_tool_denial
67
88
  end
68
89
  end
69
90
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Adapters
5
+ # What one CLI can do, declared in one place. Every adapter states every
6
+ # field: a capability inherited as a silent default is how codex was once
7
+ # assumed to refuse denied tools it never refused.
8
+ #
9
+ # @!attribute [r] compacts
10
+ # @return [Boolean] an agent on this CLI can be compacted and carried on
11
+ # in the same session
12
+ # @!attribute [r] resumes
13
+ # @return [Boolean] {Base#argv} honours {Invocation#resume}
14
+ # @!attribute [r] enforces_tool_denial
15
+ # @return [Boolean] the CLI itself refuses the denied tools
16
+ # @!attribute [r] compact_prompt
17
+ # @return [String, nil] what, sent into a resumed session, compacts it;
18
+ # nil exactly when {#compacts} is false
19
+ Capabilities = Data.define(:compacts, :resumes, :enforces_tool_denial, :compact_prompt) do
20
+ def initialize(compacts:, resumes:, enforces_tool_denial:, compact_prompt:)
21
+ if compacts == compact_prompt.nil?
22
+ raise ArgumentError, "compact_prompt must be given exactly when compacts is true"
23
+ end
24
+
25
+ super
26
+ end
27
+ end
28
+ end
29
+ end
@@ -64,10 +64,14 @@ module OpenLanes
64
64
  # it the stream reports a settled input count and a placeholder output
65
65
  # count — 2 for a four-thousand-token answer — and a spinner counting
66
66
  # what came back would read zero all run.
67
- argv = ["claude", "-p", invocation.prompt, "--add-dir", invocation.root,
68
- "--output-format", "stream-json", "--verbose", "--include-partial-messages", "--brief"]
67
+ argv = ["claude", "-p", invocation.prompt, "--add-dir", invocation.root]
68
+ invocation.dirs.each { argv += ["--add-dir", it] }
69
+ argv += ["--output-format", "stream-json", "--verbose", "--include-partial-messages"]
70
+ argv << "--brief" if invocation.brief
69
71
  argv += ["--resume", invocation.resume] if invocation.resume
70
72
  argv += LEAN.reject { invocation.slash_commands && it == SLASH_COMMANDS_OFF } if invocation.lean
73
+ argv += restricted(invocation.tools) if invocation.tools
74
+ argv += ["--append-system-prompt", invocation.system] if invocation.system
71
75
  argv += ["--disallowedTools", invocation.denied_tools.join(",")] unless invocation.denied_tools.empty?
72
76
  argv += ["--allowedTools", invocation.allowed_tools.join(",")] unless invocation.allowed_tools.empty?
73
77
  model = model(invocation.model)
@@ -79,6 +83,18 @@ module OpenLanes
79
83
  # @return [OpenLanes::Execution::Transcript]
80
84
  def transcript(trace: nil, &) = Execution::Transcript.new(trace:, &)
81
85
 
86
+ # A fixed tool set. `--tools` takes every other built-in tool away, and
87
+ # `--restricted` stops the project's and the operator's settings files
88
+ # from granting more: an allow rule in `.claude/settings.json` would
89
+ # otherwise widen what `--allowedTools` permits, and `--disallowedTools`
90
+ # only removes what somebody thought to name. `dontAsk` refuses whatever
91
+ # the allow list does not cover, rather than waiting on a prompt that
92
+ # nobody under `-p` can answer.
93
+ #
94
+ # @param tools [Array<String>]
95
+ # @return [Array<String>]
96
+ def restricted(tools) = ["--restricted", "--tools", tools.join(","), "--permission-mode", "dontAsk"]
97
+
82
98
  # @param model [String, nil] an alias (`sonnet`) or a full id
83
99
  # (`claude-fable-5-1`)
84
100
  # @return [String, nil]
@@ -89,15 +105,21 @@ module OpenLanes
89
105
  rank && rank > LADDER.index(CEILING) ? CEILING : model.to_s
90
106
  end
91
107
 
92
- # @return [Boolean]
93
- def enforces_tool_denial? = true
108
+ # `--disallowedTools` is refused by the CLI itself, and `--resume
109
+ # <session id>` carries a conversation on. `claude -p --resume <id>
110
+ # "/compact"` compacts the session and prints a `compact_boundary`
111
+ # event; the agent cannot run it on itself, because `/compact` is the
112
+ # caller's command, not a tool.
113
+ CAPABILITIES = Capabilities.new(compacts: true,
114
+ resumes: true,
115
+ enforces_tool_denial: true,
116
+ compact_prompt: "/compact")
94
117
 
95
- # `claude -p --resume <id> "/compact"` compacts the session and prints a
96
- # `compact_boundary` event; the agent cannot run it on itself, because
97
- # `/compact` is the caller's command, not a tool.
98
- #
99
- # @return [String]
100
- def compact_prompt = "/compact"
118
+ # @return [OpenLanes::Adapters::Capabilities]
119
+ def capabilities = CAPABILITIES
120
+
121
+ # @return [Array<String>]
122
+ def lean_flags = LEAN
101
123
 
102
124
  # @return [Hash{String => String}]
103
125
  def environment = { AUTOCOMPACT_VARIABLE => AUTOCOMPACT.to_s }
@@ -13,6 +13,15 @@ module OpenLanes
13
13
  # Codex says `xhigh` for the top of its scale and has no `max`.
14
14
  EFFORT = { "low" => "low", "medium" => "medium", "high" => "high", "xhigh" => "xhigh", "max" => "xhigh" }.freeze
15
15
 
16
+ # No session to resume, so nothing to compact, and no denylist of its own.
17
+ CAPABILITIES = Capabilities.new(compacts: false,
18
+ resumes: false,
19
+ enforces_tool_denial: false,
20
+ compact_prompt: nil)
21
+
22
+ # @return [OpenLanes::Adapters::Capabilities]
23
+ def capabilities = CAPABILITIES
24
+
16
25
  # @param invocation [OpenLanes::Adapters::Invocation]
17
26
  # @return [Array<String>]
18
27
  def argv(invocation)
@@ -8,6 +8,16 @@ module OpenLanes
8
8
  # Pi's `--thinking` scale already uses these words.
9
9
  THINKING = Adapters::EFFORTS
10
10
 
11
+ # `--no-session` every time: nothing resumes, nothing compacts, and
12
+ # nothing refuses a denied tool but the after-check.
13
+ CAPABILITIES = Capabilities.new(compacts: false,
14
+ resumes: false,
15
+ enforces_tool_denial: false,
16
+ compact_prompt: nil)
17
+
18
+ # @return [OpenLanes::Adapters::Capabilities]
19
+ def capabilities = CAPABILITIES
20
+
11
21
  # @param invocation [OpenLanes::Adapters::Invocation]
12
22
  # @return [Array<String>]
13
23
  def argv(invocation)
@@ -15,6 +15,15 @@ module OpenLanes
15
15
  # its events keeps working, and one that stops reporting usage shows a
16
16
  # meter at zero rather than a crash.
17
17
  class Stream
18
+ # Events that carry part of a message still being written. Only its
19
+ # finished lines are read out of one, and the closing message does not
20
+ # read them again (see {#take_partial}).
21
+ PARTIAL = /delta|update|partial|chunk/i
22
+
23
+ # Events that repeat messages already closed: pi's `turn_end` and
24
+ # `agent_end`, or anything carrying a `messages` list.
25
+ SUMMARY = /turn|agent|session|run/i
26
+
18
27
  # @param trace [String, nil] where every line is kept verbatim
19
28
  # @yieldparam progress [OpenLanes::Execution::Transcript::Progress]
20
29
  def initialize(trace: nil, &on_progress)
@@ -30,6 +39,11 @@ module OpenLanes
30
39
  @activity = nil
31
40
  @result = nil
32
41
  @error = nil
42
+ @journal = []
43
+ @heard = {}
44
+ @closed = Hash.new(0)
45
+ @open = { text: +"", taken: 0 }
46
+ @pending = Hash.new(0)
33
47
  end
34
48
 
35
49
  # @return [Integer] tokens sent, cached input included
@@ -59,6 +73,10 @@ module OpenLanes
59
73
  # @return [String, nil]
60
74
  attr_reader :trace_path
61
75
 
76
+ # @return [Array<OpenLanes::Plans::Journal::Entry, OpenLanes::Plans::Journal::Rejection>]
77
+ # the journal lines printed so far
78
+ def journal = @journal.dup
79
+
62
80
  # @return [Integer, nil]
63
81
  attr_accessor :pid
64
82
 
@@ -79,8 +97,19 @@ module OpenLanes
79
97
  # @return [Boolean]
80
98
  def failed? = !@error.nil?
81
99
 
100
+ # No sub-agents, running or spawned: codex and pi never say they delegated.
101
+ #
82
102
  # @return [OpenLanes::Execution::Transcript::Progress]
83
- def progress = Execution::Transcript::Progress.new(activity:, up:, down:, subagents: 0, pid:, message: nil)
103
+ def progress
104
+ Execution::Transcript::Progress.new(activity:,
105
+ up:,
106
+ down:,
107
+ subagents: 0,
108
+ pid:,
109
+ message: nil,
110
+ running: [],
111
+ journal: @journal.dup.freeze)
112
+ end
84
113
 
85
114
  # @param chunk [String, nil]
86
115
  # @return [void]
@@ -137,9 +166,124 @@ module OpenLanes
137
166
  @activity = said.lines.first.to_s.strip[0, Execution::Transcript::LIMIT]
138
167
  @result = said
139
168
  end
169
+ unless type.match?(/tool|command|exec/i)
170
+ if type.match?(PARTIAL)
171
+ take_partial(event)
172
+ else
173
+ take_journal(event, summary: type.match?(SUMMARY) || event.key?("messages"))
174
+ end
175
+ end
140
176
  @on_progress&.call(progress)
141
177
  end
142
178
 
179
+ # Journal lines (M2) in what the agent itself said, in every text block
180
+ # of every message it finished. Not {#text_in}, which takes the first
181
+ # `text` anywhere in the event: that can be a tool's output, which would
182
+ # make a file the agent read into its memory, and it misses a line in a
183
+ # message's second block.
184
+ #
185
+ # A CLI read by shape delivers the same message more than once: as it
186
+ # closes, and again in a summary of the turn or the run. A message with
187
+ # an id is taken once by its id. One without is always taken as it
188
+ # closes, since an agent may mean `todo U1` again after it said `done
189
+ # U1`; a summary skips as many copies of its words as have closed, and
190
+ # takes the rest.
191
+ #
192
+ # @param event [Hash]
193
+ # @param summary [Boolean] whether the event repeats closed messages
194
+ # @return [void]
195
+ def take_journal(event, summary:)
196
+ seen = Hash.new(0)
197
+ messages_in(event).each do |message|
198
+ texts = texts_of(message)
199
+ next if texts.empty?
200
+
201
+ if (id = message["id"])
202
+ next if @heard.key?(id)
203
+
204
+ @heard[id] = true
205
+ else
206
+ next if summary && (seen[texts] += 1) <= @closed[texts]
207
+
208
+ @closed[texts] += 1
209
+ end
210
+ texts.flat_map { Plans::Journal.scan(it) }.each { keep(it) }
211
+ @open = { text: +"", taken: 0 }
212
+ end
213
+ end
214
+
215
+ # The finished lines of a message still being written, as they arrive,
216
+ # so an agent killed before the message closes keeps what it printed.
217
+ # An event carries either the message so far (pi's `message`, codex's
218
+ # `item`) or only the newest piece of it (a `delta`).
219
+ #
220
+ # @param event [Hash]
221
+ # @return [void]
222
+ def take_partial(event)
223
+ whole = messages_in(event).map { texts_of(it).join("\n") }.reject(&:empty?).last
224
+ whole ? @open[:text] = +whole : @open[:text] << delta_in(event).to_s
225
+ cut = @open[:text].rindex("\n") or return
226
+
227
+ entries = Plans::Journal.scan(@open[:text][0..cut])
228
+ entries.drop(@open[:taken]).each do |entry|
229
+ @journal << entry
230
+ @pending[entry] += 1
231
+ end
232
+ @open[:taken] = entries.size
233
+ end
234
+
235
+ # An entry from a closing message, unless it was already taken from that
236
+ # message while it was being written.
237
+ #
238
+ # @param entry [OpenLanes::Plans::Journal::Entry, OpenLanes::Plans::Journal::Rejection]
239
+ # @return [void]
240
+ def keep(entry)
241
+ return @pending[entry] -= 1 if @pending[entry].positive?
242
+
243
+ @journal << entry
244
+ end
245
+
246
+ # @param event [Hash]
247
+ # @return [String, nil] the newest piece of text a partial event carries
248
+ def delta_in(event)
249
+ [event["delta"], event.dig("assistantMessageEvent", "delta")].each do |delta|
250
+ return delta if delta.is_a?(String)
251
+ return delta["text"] if delta.is_a?(Hash) && delta["text"].is_a?(String)
252
+ end
253
+ nil
254
+ end
255
+
256
+ # The agent's own messages an event carries: one under `item` (codex)
257
+ # or `message` (pi), a list under `messages`, or the event itself when
258
+ # it is a message with its words at the top level. A message that names
259
+ # a role other than the assistant's is a prompt or a tool result, and
260
+ # an item typed as anything but a message is a command, a file change
261
+ # or reasoning.
262
+ #
263
+ # @param event [Hash]
264
+ # @return [Array<Hash>]
265
+ def messages_in(event)
266
+ itself = event.key?("content") || event["text"].is_a?(String) ? event : nil
267
+ found = [event["item"], event["message"], *Array(event["messages"]), itself]
268
+ found.select do |message|
269
+ message.is_a?(Hash) &&
270
+ [nil, "assistant"].include?(message["role"]) &&
271
+ (message["type"].nil? || message["type"].to_s.match?(/message|assistant/i))
272
+ end
273
+ end
274
+
275
+ # @param message [Hash]
276
+ # @return [Array<String>] its text, block by block, tool calls left out
277
+ def texts_of(message)
278
+ content = message["content"]
279
+ blocks = if content.is_a?(Array)
280
+ content.select { it.is_a?(Hash) && [nil, "text", "output_text"].include?(it["type"]) }.map { it["text"] }
281
+ else
282
+ [message["text"], content]
283
+ end
284
+ blocks.select { it.is_a?(String) && !it.strip.empty? }
285
+ end
286
+
143
287
  # Usage counts arrive per turn, so they are added rather than replaced.
144
288
  #
145
289
  # @param event [Hash]