openlanes 3.0.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 (466) hide show
  1. checksums.yaml +7 -0
  2. data/agents/hansolo-reviewer.md +59 -0
  3. data/agents/lando-broker.md +70 -0
  4. data/agents/leah-researcher.md +60 -0
  5. data/agents/luke-backend.md +73 -0
  6. data/agents/palpatine-planner.md +61 -0
  7. data/agents/r2d2-mechanic.md +57 -0
  8. data/agents/rey-frontend.md +68 -0
  9. data/agents/yoda-writer.md +67 -0
  10. data/bin/factory-e2e +157 -0
  11. data/bin/plan-number +164 -0
  12. data/bin/setup +26 -0
  13. data/evals/cases/hansolo-reviewer/review-medium/fail/plan/plan.md +3 -0
  14. data/evals/cases/hansolo-reviewer/review-medium/fail/plan/pull-requests.md +12 -0
  15. data/evals/cases/hansolo-reviewer/review-medium/fail/plan/spec.md +5 -0
  16. data/evals/cases/hansolo-reviewer/review-medium/fail/repo/lib/cart.rb +9 -0
  17. data/evals/cases/hansolo-reviewer/review-medium/fail/repo/spec/cart_spec.rb +7 -0
  18. data/evals/cases/hansolo-reviewer/review-medium/fail/run.json +1 -0
  19. data/evals/cases/hansolo-reviewer/review-medium/pass/plan/plan.md +3 -0
  20. data/evals/cases/hansolo-reviewer/review-medium/pass/plan/pull-requests.md +15 -0
  21. data/evals/cases/hansolo-reviewer/review-medium/pass/plan/spec.md +5 -0
  22. data/evals/cases/hansolo-reviewer/review-medium/pass/repo/lib/cart.rb +9 -0
  23. data/evals/cases/hansolo-reviewer/review-medium/pass/repo/spec/cart_spec.rb +7 -0
  24. data/evals/cases/hansolo-reviewer/review-medium/pass/run.json +1 -0
  25. data/evals/cases/hansolo-reviewer/review-medium.yml +60 -0
  26. data/evals/cases/lando-broker/unblock-fast/fail/plan/blocked.md +9 -0
  27. data/evals/cases/lando-broker/unblock-fast/fail/plan/spec.md +16 -0
  28. data/evals/cases/lando-broker/unblock-fast/fail/run.json +1 -0
  29. data/evals/cases/lando-broker/unblock-fast/pass/plan/spec.md +18 -0
  30. data/evals/cases/lando-broker/unblock-fast/pass/run.json +1 -0
  31. data/evals/cases/lando-broker/unblock-fast.yml +44 -0
  32. data/evals/cases/leah-researcher/research-deep/fail/plan/spec.md +26 -0
  33. data/evals/cases/leah-researcher/research-deep/fail/run.json +1 -0
  34. data/evals/cases/leah-researcher/research-deep/pass/plan/spec.md +39 -0
  35. data/evals/cases/leah-researcher/research-deep/pass/run.json +1 -0
  36. data/evals/cases/leah-researcher/research-deep.yml +51 -0
  37. data/evals/cases/leah-researcher/research-fast/fail/plan/spec.md +27 -0
  38. data/evals/cases/leah-researcher/research-fast/fail/run.json +1 -0
  39. data/evals/cases/leah-researcher/research-fast/pass/plan/spec.md +23 -0
  40. data/evals/cases/leah-researcher/research-fast/pass/run.json +1 -0
  41. data/evals/cases/leah-researcher/research-fast.yml +55 -0
  42. data/evals/cases/luke-backend/build-medium/fail/plan/plan-backend.md +7 -0
  43. data/evals/cases/luke-backend/build-medium/fail/plan/plan-frontend.md +1 -0
  44. data/evals/cases/luke-backend/build-medium/fail/plan/plan.md +3 -0
  45. data/evals/cases/luke-backend/build-medium/fail/plan/pull-requests.md +8 -0
  46. data/evals/cases/luke-backend/build-medium/fail/plan/spec.md +8 -0
  47. data/evals/cases/luke-backend/build-medium/fail/repo/Gemfile +2 -0
  48. data/evals/cases/luke-backend/build-medium/fail/repo/lib/cart.rb +11 -0
  49. data/evals/cases/luke-backend/build-medium/fail/repo/spec/cart_spec.rb +7 -0
  50. data/evals/cases/luke-backend/build-medium/fail/run.json +1 -0
  51. data/evals/cases/luke-backend/build-medium/pass/plan/plan-backend.md +7 -0
  52. data/evals/cases/luke-backend/build-medium/pass/plan/plan-frontend.md +1 -0
  53. data/evals/cases/luke-backend/build-medium/pass/plan/plan.md +3 -0
  54. data/evals/cases/luke-backend/build-medium/pass/plan/pull-requests.md +8 -0
  55. data/evals/cases/luke-backend/build-medium/pass/plan/spec.md +8 -0
  56. data/evals/cases/luke-backend/build-medium/pass/repo/Gemfile +2 -0
  57. data/evals/cases/luke-backend/build-medium/pass/repo/lib/cart.rb +11 -0
  58. data/evals/cases/luke-backend/build-medium/pass/repo/spec/cart_spec.rb +11 -0
  59. data/evals/cases/luke-backend/build-medium/pass/run.json +1 -0
  60. data/evals/cases/luke-backend/build-medium.yml +68 -0
  61. data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan-backend.md +6 -0
  62. data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +8 -0
  63. data/evals/cases/palpatine-planner/plan-deep/fail/plan/spec.md +14 -0
  64. data/evals/cases/palpatine-planner/plan-deep/fail/run.json +1 -0
  65. data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan-backend.md +32 -0
  66. data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan-frontend.md +1 -0
  67. data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan.md +28 -0
  68. data/evals/cases/palpatine-planner/plan-deep/pass/plan/spec.md +14 -0
  69. data/evals/cases/palpatine-planner/plan-deep/pass/run.json +1 -0
  70. data/evals/cases/palpatine-planner/plan-deep.yml +50 -0
  71. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan-backend.md +48 -0
  72. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan-frontend.md +1 -0
  73. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +12 -0
  74. data/evals/cases/palpatine-planner/plan-fast/fail/plan/spec.md +12 -0
  75. data/evals/cases/palpatine-planner/plan-fast/fail/run.json +1 -0
  76. data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan-backend.md +8 -0
  77. data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan-frontend.md +1 -0
  78. data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan.md +12 -0
  79. data/evals/cases/palpatine-planner/plan-fast/pass/plan/spec.md +12 -0
  80. data/evals/cases/palpatine-planner/plan-fast/pass/run.json +1 -0
  81. data/evals/cases/palpatine-planner/plan-fast.yml +47 -0
  82. data/evals/cases/r2d2-mechanic/task-fast/fail/plan/plan.md +7 -0
  83. data/evals/cases/r2d2-mechanic/task-fast/fail/plan/pull-requests.md +6 -0
  84. data/evals/cases/r2d2-mechanic/task-fast/fail/plan/spec.md +6 -0
  85. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/README.md +3 -0
  86. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/lib/greeter/formatter.rb +12 -0
  87. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/lib/greeter.rb +15 -0
  88. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/spec/greeter_spec.rb +7 -0
  89. data/evals/cases/r2d2-mechanic/task-fast/fail/run.json +1 -0
  90. data/evals/cases/r2d2-mechanic/task-fast/pass/plan/plan.md +7 -0
  91. data/evals/cases/r2d2-mechanic/task-fast/pass/plan/pull-requests.md +6 -0
  92. data/evals/cases/r2d2-mechanic/task-fast/pass/plan/spec.md +6 -0
  93. data/evals/cases/r2d2-mechanic/task-fast/pass/repo/README.md +3 -0
  94. data/evals/cases/r2d2-mechanic/task-fast/pass/repo/lib/greeter.rb +3 -0
  95. data/evals/cases/r2d2-mechanic/task-fast/pass/repo/spec/greeter_spec.rb +7 -0
  96. data/evals/cases/r2d2-mechanic/task-fast/pass/run.json +1 -0
  97. data/evals/cases/r2d2-mechanic/task-fast.yml +59 -0
  98. data/evals/cases/r2d2-mechanic/task-medium/fail/plan/plan.md +7 -0
  99. data/evals/cases/r2d2-mechanic/task-medium/fail/plan/pull-requests.md +6 -0
  100. data/evals/cases/r2d2-mechanic/task-medium/fail/plan/spec.md +6 -0
  101. data/evals/cases/r2d2-mechanic/task-medium/fail/repo/Gemfile +2 -0
  102. data/evals/cases/r2d2-mechanic/task-medium/fail/repo/lib/greeter.rb +6 -0
  103. data/evals/cases/r2d2-mechanic/task-medium/fail/repo/spec/greeter_spec.rb +11 -0
  104. data/evals/cases/r2d2-mechanic/task-medium/fail/run.json +1 -0
  105. data/evals/cases/r2d2-mechanic/task-medium/pass/plan/plan.md +7 -0
  106. data/evals/cases/r2d2-mechanic/task-medium/pass/plan/pull-requests.md +6 -0
  107. data/evals/cases/r2d2-mechanic/task-medium/pass/plan/spec.md +6 -0
  108. data/evals/cases/r2d2-mechanic/task-medium/pass/repo/Gemfile +2 -0
  109. data/evals/cases/r2d2-mechanic/task-medium/pass/repo/lib/greeter.rb +6 -0
  110. data/evals/cases/r2d2-mechanic/task-medium/pass/repo/spec/greeter_spec.rb +11 -0
  111. data/evals/cases/r2d2-mechanic/task-medium/pass/run.json +1 -0
  112. data/evals/cases/r2d2-mechanic/task-medium.yml +58 -0
  113. data/evals/cases/rey-frontend/ui-medium/fail/plan/plan-frontend.md +7 -0
  114. data/evals/cases/rey-frontend/ui-medium/fail/plan/plan.md +4 -0
  115. data/evals/cases/rey-frontend/ui-medium/fail/plan/pull-requests.md +8 -0
  116. data/evals/cases/rey-frontend/ui-medium/fail/plan/spec.md +5 -0
  117. data/evals/cases/rey-frontend/ui-medium/fail/repo/lib/cart.rb +5 -0
  118. data/evals/cases/rey-frontend/ui-medium/fail/repo/web/cart_badge.js +4 -0
  119. data/evals/cases/rey-frontend/ui-medium/fail/repo/web/cart_badge.test.js +3 -0
  120. data/evals/cases/rey-frontend/ui-medium/fail/run.json +1 -0
  121. data/evals/cases/rey-frontend/ui-medium/pass/plan/plan-frontend.md +7 -0
  122. data/evals/cases/rey-frontend/ui-medium/pass/plan/plan.md +4 -0
  123. data/evals/cases/rey-frontend/ui-medium/pass/plan/pull-requests.md +8 -0
  124. data/evals/cases/rey-frontend/ui-medium/pass/plan/spec.md +5 -0
  125. data/evals/cases/rey-frontend/ui-medium/pass/repo/lib/cart.rb +3 -0
  126. data/evals/cases/rey-frontend/ui-medium/pass/repo/web/cart_badge.js +4 -0
  127. data/evals/cases/rey-frontend/ui-medium/pass/repo/web/cart_badge.test.js +3 -0
  128. data/evals/cases/rey-frontend/ui-medium/pass/run.json +1 -0
  129. data/evals/cases/rey-frontend/ui-medium.yml +55 -0
  130. data/evals/cases/yoda-writer/spec-medium/fail/plan/plan.md +5 -0
  131. data/evals/cases/yoda-writer/spec-medium/fail/plan/spec.md +45 -0
  132. data/evals/cases/yoda-writer/spec-medium/fail/run.json +1 -0
  133. data/evals/cases/yoda-writer/spec-medium/pass/plan/plan.md +0 -0
  134. data/evals/cases/yoda-writer/spec-medium/pass/plan/spec.md +63 -0
  135. data/evals/cases/yoda-writer/spec-medium/pass/run.json +1 -0
  136. data/evals/cases/yoda-writer/spec-medium.yml +40 -0
  137. data/exe/depot +1 -0
  138. data/exe/lanes +1 -0
  139. data/exe/openlanes +32 -0
  140. data/lib/openlanes/adapters/base.rb +69 -0
  141. data/lib/openlanes/adapters/claude.rb +124 -0
  142. data/lib/openlanes/adapters/codex.rb +33 -0
  143. data/lib/openlanes/adapters/model.rb +37 -0
  144. data/lib/openlanes/adapters/pi.rb +22 -0
  145. data/lib/openlanes/adapters/stream.rb +196 -0
  146. data/lib/openlanes/adapters.rb +92 -0
  147. data/lib/openlanes/agents/agent.rb +103 -0
  148. data/lib/openlanes/agents/profile.rb +57 -0
  149. data/lib/openlanes/agents/registry.rb +151 -0
  150. data/lib/openlanes/agents/roster.rb +103 -0
  151. data/lib/openlanes/agents/routing.rb +88 -0
  152. data/lib/openlanes/agents.rb +20 -0
  153. data/lib/openlanes/cli/agents/subcommands/describe.rb +66 -0
  154. data/lib/openlanes/cli/agents/subcommands/list.rb +20 -0
  155. data/lib/openlanes/cli/base.rb +263 -0
  156. data/lib/openlanes/cli/create/create.rb +354 -0
  157. data/lib/openlanes/cli/docs/docs.rb +40 -0
  158. data/lib/openlanes/cli/eval/eval.rb +110 -0
  159. data/lib/openlanes/cli/index/index.rb +40 -0
  160. data/lib/openlanes/cli/init/init.rb +145 -0
  161. data/lib/openlanes/cli/linear/linear.rb +36 -0
  162. data/lib/openlanes/cli/linear/subcommands/import.rb +174 -0
  163. data/lib/openlanes/cli/linear/subcommands/projects.rb +55 -0
  164. data/lib/openlanes/cli/list_plans/list_plans.rb +21 -0
  165. data/lib/openlanes/cli/mail/mail.rb +31 -0
  166. data/lib/openlanes/cli/mail/subcommands/read.rb +43 -0
  167. data/lib/openlanes/cli/mail/subcommands/send.rb +43 -0
  168. data/lib/openlanes/cli/resync/subcommands/dirs.rb +48 -0
  169. data/lib/openlanes/cli/resync/subcommands/prs.rb +111 -0
  170. data/lib/openlanes/cli/run/run.rb +544 -0
  171. data/lib/openlanes/cli/skills/subcommands/install.rb +92 -0
  172. data/lib/openlanes/cli/start/start.rb +110 -0
  173. data/lib/openlanes/cli/state/state.rb +26 -0
  174. data/lib/openlanes/cli/state/subcommands/show.rb +30 -0
  175. data/lib/openlanes/cli/state/subcommands/sign.rb +48 -0
  176. data/lib/openlanes/cli/states/states.rb +15 -0
  177. data/lib/openlanes/cli/unblock/unblock.rb +235 -0
  178. data/lib/openlanes/cli/version/version.rb +13 -0
  179. data/lib/openlanes/cli/web/web.rb +100 -0
  180. data/lib/openlanes/cli/worktree/worktree.rb +85 -0
  181. data/lib/openlanes/cli.rb +162 -0
  182. data/lib/openlanes/engine/board.rb +104 -0
  183. data/lib/openlanes/engine/dispatcher.rb +1297 -0
  184. data/lib/openlanes/engine/progress_log.rb +125 -0
  185. data/lib/openlanes/engine/runner.rb +224 -0
  186. data/lib/openlanes/engine/state_file.rb +190 -0
  187. data/lib/openlanes/engine/tally.rb +177 -0
  188. data/lib/openlanes/engine.rb +19 -0
  189. data/lib/openlanes/evals/case.rb +191 -0
  190. data/lib/openlanes/evals/changes.rb +67 -0
  191. data/lib/openlanes/evals/check.rb +224 -0
  192. data/lib/openlanes/evals/evidence.rb +71 -0
  193. data/lib/openlanes/evals/live.rb +140 -0
  194. data/lib/openlanes/evals/recording.rb +38 -0
  195. data/lib/openlanes/evals/run.rb +34 -0
  196. data/lib/openlanes/evals/score.rb +24 -0
  197. data/lib/openlanes/evals/scorer.rb +42 -0
  198. data/lib/openlanes/evals/verdict.rb +18 -0
  199. data/lib/openlanes/evals.rb +56 -0
  200. data/lib/openlanes/execution/checks.rb +73 -0
  201. data/lib/openlanes/execution/child.rb +228 -0
  202. data/lib/openlanes/execution/clock.rb +121 -0
  203. data/lib/openlanes/execution/control.rb +194 -0
  204. data/lib/openlanes/execution/executor.rb +849 -0
  205. data/lib/openlanes/execution/prompt.rb +455 -0
  206. data/lib/openlanes/execution/transcript.rb +531 -0
  207. data/lib/openlanes/execution.rb +19 -0
  208. data/lib/openlanes/launcher.rb +69 -0
  209. data/lib/openlanes/lifecycle/adoption.rb +197 -0
  210. data/lib/openlanes/lifecycle/brief.rb +288 -0
  211. data/lib/openlanes/lifecycle/completion.rb +58 -0
  212. data/lib/openlanes/lifecycle/creator.rb +126 -0
  213. data/lib/openlanes/lifecycle/lanes.rb +74 -0
  214. data/lib/openlanes/lifecycle/resync.rb +428 -0
  215. data/lib/openlanes/lifecycle/unblocker.rb +236 -0
  216. data/lib/openlanes/lifecycle.rb +28 -0
  217. data/lib/openlanes/linear/api.rb +224 -0
  218. data/lib/openlanes/linear/attribution.rb +185 -0
  219. data/lib/openlanes/linear/fuzzy.rb +68 -0
  220. data/lib/openlanes/linear/import.rb +314 -0
  221. data/lib/openlanes/linear/issue.rb +187 -0
  222. data/lib/openlanes/linear/mapping.rb +116 -0
  223. data/lib/openlanes/linear/push.rb +197 -0
  224. data/lib/openlanes/linear/survey.rb +174 -0
  225. data/lib/openlanes/linear/unit.rb +274 -0
  226. data/lib/openlanes/linear.rb +41 -0
  227. data/lib/openlanes/live/claims.rb +71 -0
  228. data/lib/openlanes/live/command.rb +122 -0
  229. data/lib/openlanes/live/commands.rb +505 -0
  230. data/lib/openlanes/live/keys.rb +93 -0
  231. data/lib/openlanes/live/memory_store.rb +265 -0
  232. data/lib/openlanes/live/meter.rb +51 -0
  233. data/lib/openlanes/live/node.rb +88 -0
  234. data/lib/openlanes/live/publisher.rb +179 -0
  235. data/lib/openlanes/live/repo.rb +60 -0
  236. data/lib/openlanes/live/run_record.rb +133 -0
  237. data/lib/openlanes/live/run_state.rb +101 -0
  238. data/lib/openlanes/live/session.rb +107 -0
  239. data/lib/openlanes/live/snapshot.rb +322 -0
  240. data/lib/openlanes/live/stations.rb +67 -0
  241. data/lib/openlanes/live/store.rb +277 -0
  242. data/lib/openlanes/live.rb +109 -0
  243. data/lib/openlanes/plans/dev_work.rb +56 -0
  244. data/lib/openlanes/plans/feature.rb +131 -0
  245. data/lib/openlanes/plans/index.rb +208 -0
  246. data/lib/openlanes/plans/ledger.rb +255 -0
  247. data/lib/openlanes/plans/mailbox.rb +164 -0
  248. data/lib/openlanes/plans/ordinal.rb +92 -0
  249. data/lib/openlanes/plans/plan_state.rb +291 -0
  250. data/lib/openlanes/plans/pull_request.rb +29 -0
  251. data/lib/openlanes/plans/pull_requests.rb +229 -0
  252. data/lib/openlanes/plans/spec.rb +213 -0
  253. data/lib/openlanes/plans/state_machine.rb +433 -0
  254. data/lib/openlanes/plans/status.rb +365 -0
  255. data/lib/openlanes/plans/subject.rb +133 -0
  256. data/lib/openlanes/plans/tree.rb +79 -0
  257. data/lib/openlanes/plans.rb +39 -0
  258. data/lib/openlanes/presentation/diagram.rb +149 -0
  259. data/lib/openlanes/presentation/documentation.rb +438 -0
  260. data/lib/openlanes/presentation/reporter.rb +181 -0
  261. data/lib/openlanes/presentation/viewer.rb +62 -0
  262. data/lib/openlanes/presentation.rb +23 -0
  263. data/lib/openlanes/skills/installer.rb +68 -0
  264. data/lib/openlanes/skills/skill.rb +60 -0
  265. data/lib/openlanes/skills.rb +25 -0
  266. data/lib/openlanes/support/config.rb +217 -0
  267. data/lib/openlanes/support/frontmatter.rb +38 -0
  268. data/lib/openlanes/support/markdown.rb +56 -0
  269. data/lib/openlanes/support/project.rb +79 -0
  270. data/lib/openlanes/support/ui.rb +671 -0
  271. data/lib/openlanes/vcs/github.rb +171 -0
  272. data/lib/openlanes/vcs/publisher.rb +221 -0
  273. data/lib/openlanes/vcs/worktree.rb +248 -0
  274. data/lib/openlanes/vcs.rb +19 -0
  275. data/lib/openlanes/version.rb +10 -0
  276. data/lib/openlanes/web/app.rb +176 -0
  277. data/lib/openlanes/web/context.rb +66 -0
  278. data/lib/openlanes/web/demo.rb +169 -0
  279. data/lib/openlanes/web/demo_board.rb +286 -0
  280. data/lib/openlanes/web/events.rb +104 -0
  281. data/lib/openlanes/web/launcher.rb +143 -0
  282. data/lib/openlanes/web/server.rb +77 -0
  283. data/lib/openlanes/web/stores.rb +36 -0
  284. data/lib/openlanes/web.rb +92 -0
  285. data/lib/openlanes.rb +151 -0
  286. data/openlanes.gemspec +87 -0
  287. data/schemas/plan-state.schema.json +101 -0
  288. data/skills/openlanes/SKILL.md +48 -0
  289. data/skills/openlanes-documents/SKILL.md +61 -0
  290. data/skills/openlanes-documents/references/blocked-template.md +49 -0
  291. data/skills/openlanes-documents/references/closing-templates.md +59 -0
  292. data/skills/openlanes-documents/references/plan-template.md +61 -0
  293. data/skills/openlanes-documents/references/retroactive-spec.md +43 -0
  294. data/skills/openlanes-documents/references/spec-template.md +45 -0
  295. data/skills/openlanes-run/SKILL.md +90 -0
  296. data/skills/openlanes-setup/SKILL.md +64 -0
  297. data/skills/openlanes-spec/SKILL.md +90 -0
  298. data/skills/openlanes-status/SKILL.md +32 -0
  299. data/skills/openlanes-sync/SKILL.md +22 -0
  300. data/web/dist/assets/apl-B4CMkyY2.js +2 -0
  301. data/web/dist/assets/asciiarmor-Df11BRmG.js +2 -0
  302. data/web/dist/assets/asn1-EdZsLKOL.js +2 -0
  303. data/web/dist/assets/asterisk-B-8jnY81.js +2 -0
  304. data/web/dist/assets/barlow-condensed-latin-600-normal-BFJEwTuo.woff +0 -0
  305. data/web/dist/assets/barlow-condensed-latin-600-normal-DepVgxBB.woff2 +0 -0
  306. data/web/dist/assets/barlow-condensed-latin-ext-600-normal-18ESti3H.woff2 +0 -0
  307. data/web/dist/assets/barlow-condensed-latin-ext-600-normal-Clv9cIcR.woff +0 -0
  308. data/web/dist/assets/barlow-condensed-vietnamese-600-normal-A5AYRdjN.woff2 +0 -0
  309. data/web/dist/assets/barlow-condensed-vietnamese-600-normal-CNlPk46_.woff +0 -0
  310. data/web/dist/assets/brainfuck-C4LP7Hcl.js +2 -0
  311. data/web/dist/assets/clike-B9uivgTg.js +2 -0
  312. data/web/dist/assets/clojure-BMjYHr_A.js +2 -0
  313. data/web/dist/assets/cmake-BQqOBYOt.js +2 -0
  314. data/web/dist/assets/cobol-CWcv1MsR.js +2 -0
  315. data/web/dist/assets/coffeescript-S37ZYGWr.js +2 -0
  316. data/web/dist/assets/commonlisp-DBKNyK5s.js +2 -0
  317. data/web/dist/assets/crystal-SjHAIU92.js +2 -0
  318. data/web/dist/assets/css-BnMrqG3P.js +2 -0
  319. data/web/dist/assets/cypher-C_CwsFkJ.js +2 -0
  320. data/web/dist/assets/d-pRatUO7H.js +2 -0
  321. data/web/dist/assets/diff-DbItnlRl.js +2 -0
  322. data/web/dist/assets/dockerfile-BKs6k2Af.js +2 -0
  323. data/web/dist/assets/dtd-DF_7sFjM.js +2 -0
  324. data/web/dist/assets/dylan-DwRh75JA.js +2 -0
  325. data/web/dist/assets/ebnf-CDyGwa7X.js +2 -0
  326. data/web/dist/assets/ecl-Cabwm37j.js +2 -0
  327. data/web/dist/assets/eiffel-CnydiIhH.js +2 -0
  328. data/web/dist/assets/elm-vLlmbW-K.js +2 -0
  329. data/web/dist/assets/erlang-BNw1qcRV.js +2 -0
  330. data/web/dist/assets/factor-kuTfRLto.js +2 -0
  331. data/web/dist/assets/favicon-C8pAuqqr.png +0 -0
  332. data/web/dist/assets/fcl-Kvtd6kyn.js +2 -0
  333. data/web/dist/assets/forth-Ffai-XNe.js +2 -0
  334. data/web/dist/assets/fortran-DYz_wnZ1.js +2 -0
  335. data/web/dist/assets/gas-Bneqetm1.js +2 -0
  336. data/web/dist/assets/gherkin-heZmZLOM.js +2 -0
  337. data/web/dist/assets/groovy-D9Dt4D0W.js +2 -0
  338. data/web/dist/assets/haskell-Cw1EW3IL.js +2 -0
  339. data/web/dist/assets/haxe-H-WmDvRZ.js +2 -0
  340. data/web/dist/assets/http-DBlCnlav.js +2 -0
  341. data/web/dist/assets/idl-BEugSyMb.js +2 -0
  342. data/web/dist/assets/index-2xPC-eHt.js +2 -0
  343. data/web/dist/assets/index-8dNhRmiO.js +8 -0
  344. data/web/dist/assets/index-B7-O9ujQ.js +2 -0
  345. data/web/dist/assets/index-BdR34cXC.js +2 -0
  346. data/web/dist/assets/index-Bp7W-ZTq.js +2 -0
  347. data/web/dist/assets/index-Byyxbh_z.js +4 -0
  348. data/web/dist/assets/index-C0VmkrO-.js +2 -0
  349. data/web/dist/assets/index-CI74IkWP.js +198 -0
  350. data/web/dist/assets/index-CYxcbpRM.js +3 -0
  351. data/web/dist/assets/index-D0rmJN3h.js +2 -0
  352. data/web/dist/assets/index-DB4QwuYn.js +2 -0
  353. data/web/dist/assets/index-DDI7zoAS.js +2 -0
  354. data/web/dist/assets/index-DGLUuEXi.js +2 -0
  355. data/web/dist/assets/index-OR-odsl9.js +2 -0
  356. data/web/dist/assets/index-P3vSkiU-.js +2 -0
  357. data/web/dist/assets/index-a4EHR-u4.css +1 -0
  358. data/web/dist/assets/index-mRV4XhJZ.js +2 -0
  359. data/web/dist/assets/index-popk9wbW.js +2 -0
  360. data/web/dist/assets/index-uM88Njdj.js +2 -0
  361. data/web/dist/assets/javascript-iXu5QeM3.js +2 -0
  362. data/web/dist/assets/julia-DuME0IfC.js +2 -0
  363. data/web/dist/assets/livescript-BwQOo05w.js +2 -0
  364. data/web/dist/assets/logo-on-dark-DI9gCdN4.png +0 -0
  365. data/web/dist/assets/logo-on-light-BciwqxcJ.png +0 -0
  366. data/web/dist/assets/lua-VAEuO923.js +2 -0
  367. data/web/dist/assets/mathematica-DTrFuWx2.js +2 -0
  368. data/web/dist/assets/mbox-CNhZ1qSd.js +2 -0
  369. data/web/dist/assets/mirc-CjQqDB4T.js +2 -0
  370. data/web/dist/assets/mllike-CXdrOF99.js +2 -0
  371. data/web/dist/assets/modelica-Dc1JOy9r.js +2 -0
  372. data/web/dist/assets/mscgen-BA5vi2Kp.js +2 -0
  373. data/web/dist/assets/mumps-BT43cFF4.js +2 -0
  374. data/web/dist/assets/nginx-DdIZxoE0.js +2 -0
  375. data/web/dist/assets/nsis-LY0VT_u5.js +2 -0
  376. data/web/dist/assets/ntriples-BfvgReVJ.js +2 -0
  377. data/web/dist/assets/octave-Ck1zUtKM.js +2 -0
  378. data/web/dist/assets/overpass-cyrillic-400-normal-CZsFKZCk.woff +0 -0
  379. data/web/dist/assets/overpass-cyrillic-400-normal-tXDxWGRy.woff2 +0 -0
  380. data/web/dist/assets/overpass-cyrillic-600-normal-B-hc8bdn.woff2 +0 -0
  381. data/web/dist/assets/overpass-cyrillic-600-normal-CXB2Mrig.woff +0 -0
  382. data/web/dist/assets/overpass-cyrillic-800-normal-CP6RWG8X.woff2 +0 -0
  383. data/web/dist/assets/overpass-cyrillic-800-normal-PJW4A9xW.woff +0 -0
  384. data/web/dist/assets/overpass-cyrillic-ext-400-normal-AKUz2ZI5.woff2 +0 -0
  385. data/web/dist/assets/overpass-cyrillic-ext-400-normal-C2ZjKngx.woff +0 -0
  386. data/web/dist/assets/overpass-cyrillic-ext-600-normal-CW4Fknsm.woff2 +0 -0
  387. data/web/dist/assets/overpass-cyrillic-ext-600-normal-Zx-mtRSD.woff +0 -0
  388. data/web/dist/assets/overpass-cyrillic-ext-800-normal-CIth-cA7.woff2 +0 -0
  389. data/web/dist/assets/overpass-cyrillic-ext-800-normal-hx3cL3ZN.woff +0 -0
  390. data/web/dist/assets/overpass-latin-400-normal-BpeLJ0bs.woff2 +0 -0
  391. data/web/dist/assets/overpass-latin-400-normal-c_C8zulu.woff +0 -0
  392. data/web/dist/assets/overpass-latin-600-normal-25RhTNCi.woff2 +0 -0
  393. data/web/dist/assets/overpass-latin-600-normal-DhYQrzaY.woff +0 -0
  394. data/web/dist/assets/overpass-latin-800-normal-Bzx_XP5l.woff2 +0 -0
  395. data/web/dist/assets/overpass-latin-800-normal-D6-q4f0l.woff +0 -0
  396. data/web/dist/assets/overpass-latin-ext-400-normal-BIkXRwl6.woff2 +0 -0
  397. data/web/dist/assets/overpass-latin-ext-400-normal-DUq_h-_z.woff +0 -0
  398. data/web/dist/assets/overpass-latin-ext-600-normal-Cyj4Nio0.woff2 +0 -0
  399. data/web/dist/assets/overpass-latin-ext-600-normal-DQKKMGzp.woff +0 -0
  400. data/web/dist/assets/overpass-latin-ext-800-normal-D6WKWGNs.woff +0 -0
  401. data/web/dist/assets/overpass-latin-ext-800-normal-DZ60AB77.woff2 +0 -0
  402. data/web/dist/assets/overpass-mono-cyrillic-400-normal-B144qALP.woff2 +0 -0
  403. data/web/dist/assets/overpass-mono-cyrillic-400-normal-XK9_jwT8.woff +0 -0
  404. data/web/dist/assets/overpass-mono-cyrillic-ext-400-normal-Dgejvo4A.woff2 +0 -0
  405. data/web/dist/assets/overpass-mono-cyrillic-ext-400-normal-DgxwtUZy.woff +0 -0
  406. data/web/dist/assets/overpass-mono-latin-400-normal-DqAOLzCO.woff +0 -0
  407. data/web/dist/assets/overpass-mono-latin-400-normal-VINZG6Js.woff2 +0 -0
  408. data/web/dist/assets/overpass-mono-latin-ext-400-normal-5YIw-E8g.woff +0 -0
  409. data/web/dist/assets/overpass-mono-latin-ext-400-normal-DBdJzBs_.woff2 +0 -0
  410. data/web/dist/assets/overpass-mono-vietnamese-400-normal-DrLp_cHj.woff +0 -0
  411. data/web/dist/assets/overpass-mono-vietnamese-400-normal-jmOeL0rj.woff2 +0 -0
  412. data/web/dist/assets/overpass-vietnamese-400-normal-BKRCEiXy.woff2 +0 -0
  413. data/web/dist/assets/overpass-vietnamese-400-normal-CCWAWVG7.woff +0 -0
  414. data/web/dist/assets/overpass-vietnamese-600-normal-CjS-4vQf.woff2 +0 -0
  415. data/web/dist/assets/overpass-vietnamese-600-normal-DlMMlPfM.woff +0 -0
  416. data/web/dist/assets/overpass-vietnamese-800-normal-BB8K9-Vo.woff2 +0 -0
  417. data/web/dist/assets/overpass-vietnamese-800-normal-DZUygvj_.woff +0 -0
  418. data/web/dist/assets/oz-BzwKVEFT.js +2 -0
  419. data/web/dist/assets/pascal--L3eBynH.js +2 -0
  420. data/web/dist/assets/perl-CdXCOZ3F.js +2 -0
  421. data/web/dist/assets/pig-CevX1Tat.js +2 -0
  422. data/web/dist/assets/powershell-CFHJl5sT.js +2 -0
  423. data/web/dist/assets/properties-C78fOPTZ.js +2 -0
  424. data/web/dist/assets/protobuf-ChK-085T.js +2 -0
  425. data/web/dist/assets/pug-DeIclll2.js +2 -0
  426. data/web/dist/assets/puppet-DMA9R1ak.js +2 -0
  427. data/web/dist/assets/python-BuPzkPfP.js +2 -0
  428. data/web/dist/assets/q-pXgVlZs6.js +2 -0
  429. data/web/dist/assets/r-B6wPVr8A.js +2 -0
  430. data/web/dist/assets/rpm-CTu-6PCP.js +2 -0
  431. data/web/dist/assets/ruby-BK5aLJev.js +2 -0
  432. data/web/dist/assets/sas-B4kiWyti.js +2 -0
  433. data/web/dist/assets/scheme-C41bIUwD.js +2 -0
  434. data/web/dist/assets/shell-CjFT_Tl9.js +2 -0
  435. data/web/dist/assets/sieve-C3Gn_uJK.js +2 -0
  436. data/web/dist/assets/simple-mode-GW_nhZxv.js +2 -0
  437. data/web/dist/assets/smalltalk-CnHTOXQT.js +2 -0
  438. data/web/dist/assets/solr-DehyRSwq.js +2 -0
  439. data/web/dist/assets/sparql-DkYu6x3z.js +2 -0
  440. data/web/dist/assets/spreadsheet-BCZA_wO0.js +2 -0
  441. data/web/dist/assets/sql-D0XecflT.js +2 -0
  442. data/web/dist/assets/stex-C3f8Ysf7.js +2 -0
  443. data/web/dist/assets/stylus-B533Al4x.js +2 -0
  444. data/web/dist/assets/swift-BzpIVaGY.js +2 -0
  445. data/web/dist/assets/tcl-DVfN8rqt.js +2 -0
  446. data/web/dist/assets/textile-CnDTJFAw.js +2 -0
  447. data/web/dist/assets/tiddlywiki-DO-Gjzrf.js +2 -0
  448. data/web/dist/assets/tiki-DGYXhP31.js +2 -0
  449. data/web/dist/assets/toml-Bm5Em-hy.js +2 -0
  450. data/web/dist/assets/troff-wAsdV37c.js +2 -0
  451. data/web/dist/assets/ttcn-CfJYG6tj.js +2 -0
  452. data/web/dist/assets/ttcn-cfg-B9xdYoR4.js +2 -0
  453. data/web/dist/assets/turtle-B1tBg_DP.js +2 -0
  454. data/web/dist/assets/vb-CmGdzxic.js +2 -0
  455. data/web/dist/assets/vbscript-BuJXcnF6.js +2 -0
  456. data/web/dist/assets/velocity-D8B20fx6.js +2 -0
  457. data/web/dist/assets/verilog-C6RDOZhf.js +2 -0
  458. data/web/dist/assets/vhdl-lSbBsy5d.js +2 -0
  459. data/web/dist/assets/webidl-ZXfAyPTL.js +2 -0
  460. data/web/dist/assets/xquery-DzFWVndE.js +2 -0
  461. data/web/dist/assets/yacas-BJ4BC0dw.js +2 -0
  462. data/web/dist/assets/z80-Hz9HOZM7.js +2 -0
  463. data/web/dist/index.html +14 -0
  464. data/web/dist/vite.svg +1 -0
  465. data/web/src/api/fixtures/board.json +512 -0
  466. metadata +907 -0
data/bin/factory-e2e ADDED
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env bash
2
+ # vim: ft=bash
3
+ #
4
+ # The factory integration test: a throwaway git repository, `openlanes start`
5
+ # serving it, and Cypress building a tic-tac-toe page through the control
6
+ # room with real agents. It spends real tokens, so it refuses to run unless
7
+ # OPENLANES_FACTORY_E2E=1 says so, and nothing in `just test`, `just web-e2e`
8
+ # or CircleCI calls it.
9
+ #
10
+ # OPENLANES_FACTORY_E2E=1 just factory-e2e
11
+ #
12
+ # Settings, each with a default:
13
+ #
14
+ # FACTORY_PORT control room port (4777)
15
+ # FACTORY_REDIS_URL Redis for the board and its commands (redis://localhost:6379/11)
16
+ # FACTORY_MODEL model every agent runs on (haiku)
17
+ # FACTORY_TIMEOUT seconds before one agent is abandoned (600)
18
+ # FACTORY_MINUTES minutes Cypress waits for the build (15)
19
+ #
20
+ # What it leaves behind is log/factory-e2e/: the progress log, the last
21
+ # board, every plan's state.json and spec.md, the built index.html, and the
22
+ # tail of each agent's trace. The temp repository and the `openlanes start` it
23
+ # launched are always torn down, pass or fail; nothing it did not start is
24
+ # touched.
25
+
26
+ set -euo pipefail
27
+
28
+ repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
29
+
30
+ if [[ "${OPENLANES_FACTORY_E2E:-}" != "1" ]]; then
31
+ echo "factory-e2e runs real agents and spends real tokens." >&2
32
+ echo "Set OPENLANES_FACTORY_E2E=1 to run it: OPENLANES_FACTORY_E2E=1 just factory-e2e" >&2
33
+ exit 64
34
+ fi
35
+
36
+ port="${FACTORY_PORT:-4777}"
37
+ redis_url="${FACTORY_REDIS_URL:-redis://localhost:6379/11}"
38
+ model="${FACTORY_MODEL:-haiku}"
39
+ timeout="${FACTORY_TIMEOUT:-600}"
40
+ minutes="${FACTORY_MINUTES:-15}"
41
+ artifacts="${repo}/log/factory-e2e"
42
+
43
+ command -v claude >/dev/null || { echo "claude is not on PATH; the factory has no agents to run." >&2; exit 69; }
44
+ if command -v redis-cli >/dev/null && ! redis-cli -u "${redis_url}" ping >/dev/null 2>&1; then
45
+ echo "Redis does not answer on ${redis_url}." >&2
46
+ exit 69
47
+ fi
48
+ # Another server on the port (the user's own `openlanes web` on 11000, say) is
49
+ # left alone: the run refuses rather than share or kill it.
50
+ if lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1; then
51
+ echo "Port ${port} is taken; set FACTORY_PORT to a free one." >&2
52
+ exit 69
53
+ fi
54
+ [[ -f "${repo}/web/dist/index.html" ]] || { echo "web/dist is missing; run just web-build first." >&2; exit 66; }
55
+
56
+ tmp="$(mktemp -d "${TMPDIR:-/tmp}/openlanes-factory.XXXXXX")"
57
+ project="${tmp}/tictactoe"
58
+ started_at="${tmp}/.started"
59
+ pid=""
60
+
61
+ openlanes() { "${repo}/exe/openlanes" "$@"; }
62
+
63
+ collect() {
64
+ rm -rf "${artifacts}"
65
+ mkdir -p "${artifacts}/plans" "${artifacts}/traces"
66
+ curl -sf "http://127.0.0.1:${port}/api/board" -o "${artifacts}/board.json" 2>/dev/null || true
67
+ cp "${tmp}/progress.log" "${artifacts}/" 2>/dev/null || true
68
+ cp "${tmp}/start.log" "${artifacts}/" 2>/dev/null || true
69
+ if [[ -d "${project}/.plans" ]]; then
70
+ for plan in "${project}"/.plans/*/; do
71
+ name="$(basename "${plan}")"
72
+ mkdir -p "${artifacts}/plans/${name}"
73
+ cp "${plan}"*.md "${plan}"state.json "${artifacts}/plans/${name}/" 2>/dev/null || true
74
+ done
75
+ fi
76
+ find "${project}" -name index.html -not -path '*/.git/*' -not -path '*/node_modules/*' \
77
+ -exec cp {} "${artifacts}/" \; 2>/dev/null || true
78
+ (cd "${project}" 2>/dev/null && git status --short >"${artifacts}/git-status.txt") || true
79
+ # The traces live outside every repository (Executor::TRACE_DIR); keep the
80
+ # tail of the ones this run wrote.
81
+ traces="${TMPDIR:-/tmp}"
82
+ traces="${traces%/}/openlanes-traces"
83
+ if [[ -d "${traces}" ]]; then
84
+ find "${traces}" -type f -newer "${started_at}" | while read -r trace; do
85
+ tail -n 40 "${trace}" >"${artifacts}/traces/$(basename "${trace}")"
86
+ done
87
+ fi
88
+ }
89
+
90
+ teardown() {
91
+ status=$?
92
+ trap - EXIT INT TERM
93
+ collect || true
94
+ if [[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null; then
95
+ # TERM is `openlanes start`'s graceful stop: its agents are told to stop and
96
+ # its control room goes down with it. KILL only if it outlives that.
97
+ kill -TERM "${pid}" 2>/dev/null || true
98
+ for _ in $(seq 1 30); do kill -0 "${pid}" 2>/dev/null || break; sleep 1; done
99
+ kill -KILL "${pid}" 2>/dev/null || true
100
+ fi
101
+ rm -rf "${tmp}"
102
+ echo "factory-e2e: artifacts in ${artifacts}" >&2
103
+ exit "${status}"
104
+ }
105
+ trap teardown EXIT
106
+ trap 'exit 130' INT
107
+ trap 'exit 143' TERM
108
+
109
+ touch "${started_at}"
110
+
111
+ # A repository with one commit and `openlanes init`, and no remote: nothing this
112
+ # run does can reach GitHub.
113
+ mkdir -p "${project}"
114
+ (
115
+ cd "${project}"
116
+ git init -q -b main
117
+ git config user.name "Factory Test"
118
+ git config user.email "factory-test@example.invalid"
119
+ cat >README.md <<'MARKDOWN'
120
+ # Tic Tac Toe
121
+
122
+ A static web page: plain HTML, CSS and JavaScript in the repository root,
123
+ opened straight from disk. There is no package manager, no build step and no
124
+ test runner; check a change by reading it and opening the page.
125
+ MARKDOWN
126
+ git add README.md
127
+ git commit -q -m "Initial commit"
128
+ openlanes init -y -q --project "Tic Tac Toe" -j 1
129
+ git add -A
130
+ git commit -q -m "Initialize openlanes"
131
+ )
132
+
133
+ # `--isolation shared` is the existing local-only mode: one tree, no branch
134
+ # per plan, and no publisher, so nothing is pushed and no pull request is
135
+ # opened. The finished build stays at 🟡 Building, done for this run: 🟢
136
+ # Ready for Review needs an open pull request.
137
+ (
138
+ cd "${project}"
139
+ exec "${repo}/exe/openlanes" start --no-open \
140
+ --port "${port}" --redis-url "${redis_url}" \
141
+ --isolation shared -j 1 \
142
+ --model "${model}" --timeout "${timeout}" \
143
+ --log "${tmp}/progress.log"
144
+ ) >"${tmp}/start.log" 2>&1 &
145
+ pid=$!
146
+
147
+ for _ in $(seq 1 120); do
148
+ curl -sf "http://127.0.0.1:${port}/healthz" >/dev/null 2>&1 && break
149
+ kill -0 "${pid}" 2>/dev/null || { echo "openlanes start exited:" >&2; cat "${tmp}/start.log" >&2; exit 1; }
150
+ sleep 0.5
151
+ done
152
+ curl -sf "http://127.0.0.1:${port}/healthz" >/dev/null || { echo "The control room never answered on ${port}." >&2; exit 1; }
153
+
154
+ cd "${repo}/web"
155
+ npx cypress run --config-file cypress.factory.config.ts \
156
+ --config "baseUrl=http://127.0.0.1:${port}" \
157
+ --env "projectDir=${project},minutes=${minutes}"
data/bin/plan-number ADDED
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # plan-number - resolve the .plans folder a branch belongs to, or fail.
4
+ #
5
+ # plan-number # resolve for the current branch
6
+ # plan-number -b <branch> # ...for another branch
7
+ # plan-number -D <dir> # .plans lives elsewhere (default: ./.plans)
8
+ # plan-number --title "Foo" # print "[003] Foo" instead of "003"
9
+ # # (or "[018.01] Foo" for a retroactive plan)
10
+ # plan-number --none # print "XXX": this PR implements no plan
11
+ #
12
+ # Exit 0 and print the number on stdout when exactly one plan can be resolved.
13
+ # Exit non-zero and explain on stderr otherwise. It never picks a winner from
14
+ # several candidates and never invents one.
15
+ #
16
+ # == Why it refuses rather than guesses
17
+ #
18
+ # The number ends up in a PR title, and pull-requests.md is generated pfrom those
19
+ # titles. A wrong number does not announce itself: it files the work under a
20
+ # plan that did not do it, and the plan that did looks untouched. That is worse
21
+ # than an unprefixed title, which at least reads as "unknown" rather than as a
22
+ # confident lie. Every failure below is therefore loud.
23
+ #
24
+ # == The two shapes of number
25
+ #
26
+ # NNN an ordinary plan, specified before it was built.
27
+ # NNN.MM a RETROACTIVE plan: the work shipped first and the specification
28
+ # was written afterwards to describe what exists. It sits in the
29
+ # sequence where the work landed - 018.01 merged after 018 and
30
+ # before 019.
31
+ #
32
+ # NNN.MM does NOT mean "part of NNN". The dot reads as containment in most
33
+ # numbering schemes and here it does not: 018.01 is a sibling of 018 that
34
+ # arrived later, with no relationship to its subject. See .plans/README.md.
35
+ #
36
+ # The decimal is ALWAYS two digits. One digit would sort into the middle of the
37
+ # two-digit range - 018.09 < 018.1 < 018.10 - so a single mixed-width folder
38
+ # silently reorders the index. Two digits also give 99 slots per gap, which
39
+ # retires the question of running out: a gap closes the moment the next plan is
40
+ # created, since nothing merging today can land between 018 and 019.
41
+ #
42
+ # == The resolution order, strongest signal first
43
+ #
44
+ # 1. The BRANCH NAME. `kig/021-add-tune-appender` says 021 and says it before a
45
+ # single line is written, which is the only moment the author certainly knows
46
+ # the answer. Set once, carried on the PR record forever, survives squash.
47
+ #
48
+ # 2. The DIFF against the base, but ONLY when it touches exactly one plan
49
+ # folder. Measured across this repository's 24 merged pull requests, this
50
+ # alone resolves 6 and is ambiguous or silent on 18, so it is a fallback and
51
+ # never a primary. Files directly under .plans/ (BACKLOG.md, README.md,
52
+ # INDEX.md) are ignored: they belong to no single plan.
53
+ #
54
+ # 3. Nothing else. There is no "closest match by topic", because a heuristic
55
+ # that is right most of the time produces an index nobody can trust and
56
+ # nobody can audit.
57
+ #
58
+ # XXX is deliberately NOT step 4. A PR that implements no plan is a legitimate
59
+ # and common thing - dependency bumps, CI work, hotfixes - and it is marked
60
+ # [XXX] so that "no plan" is visibly asserted rather than merely absent. But the
61
+ # author asserts it; this script will not, because emitting XXX on a failed
62
+ # lookup would launder "I could not tell" into "there is definitely none", which
63
+ # is the same lie as guessing a number, told in the other direction.
64
+ #
65
+ # © 2026 Konstantin Gredeskoul
66
+
67
+ set -euo pipefail
68
+
69
+ PLANS_DIR=".plans"
70
+ BRANCH=""
71
+ TITLE=""
72
+ NONE=0
73
+ BASE="${PLAN_NUMBER_BASE:-origin/main}"
74
+
75
+ die() { printf '%s\n' "$*" >&2; exit 1; }
76
+
77
+ usage() {
78
+ cat >&2 <<'USAGE'
79
+ Usage: plan-number [-D <plans-dir>] [-b <branch>] [--base <ref>] [--title <text>] [--none]
80
+
81
+ Prints the plan number for the branch (NNN, or NNN.MM when the plan was written
82
+ retroactively), or fails loudly.
83
+
84
+ With --title, prints "[NNN] <text>" ready to hand to `gh pr create -t`.
85
+ With --none, prints XXX: an assertion that this PR implements no plan.
86
+ USAGE
87
+ exit 2
88
+ }
89
+
90
+ while [[ $# -gt 0 ]]; do
91
+ case "$1" in
92
+ -D|--dir) PLANS_DIR="${2:?}"; shift 2 ;;
93
+ -b|--branch) BRANCH="${2:?}"; shift 2 ;;
94
+ --base) BASE="${2:?}"; shift 2 ;;
95
+ -t|--title) TITLE="${2:?}"; shift 2 ;;
96
+ -n|--none) NONE=1; shift ;;
97
+ -h|--help) usage ;;
98
+ *) die "plan-number: unknown argument: $1" ;;
99
+ esac
100
+ done
101
+
102
+ # The three-digit number, optionally followed by one retroactive digit.
103
+ NUM_RE='[0-9]{3}(\.[0-9]{2})?'
104
+
105
+ if [[ "$NONE" == "1" ]]; then
106
+ if [[ -n "$TITLE" ]]; then printf '[XXX] %s\n' "$TITLE"; else printf 'XXX\n'; fi
107
+ exit 0
108
+ fi
109
+
110
+ [[ -d "$PLANS_DIR" ]] || die "plan-number: no $PLANS_DIR here. Run this from the repository root, or pass -D."
111
+
112
+ # Every plan number that actually exists on disk. A resolved number is checked
113
+ # against this, so a typo in a branch name fails here rather than in the index
114
+ # six weeks later.
115
+ existing="$(find "$PLANS_DIR" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; \
116
+ | grep -oE "^${NUM_RE}" | sort -u)"
117
+ [[ -n "$existing" ]] || die "plan-number: $PLANS_DIR holds no NNN[.MM]-<status>-<slug> folders."
118
+
119
+ exists() { printf '%s\n' "$existing" | grep -qx "$1"; }
120
+
121
+ emit() {
122
+ local n="$1"
123
+ exists "$n" || die "plan-number: resolved $n, but $PLANS_DIR has no folder $n-*.
124
+ Existing plans: $(printf '%s' "$existing" | tr '\n' ' ')"
125
+ if [[ -n "$TITLE" ]]; then printf '[%s] %s\n' "$n" "$TITLE"; else printf '%s\n' "$n"; fi
126
+ exit 0
127
+ }
128
+
129
+ [[ -n "$BRANCH" ]] || BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
130
+
131
+ # 1. The branch name. Matches 021 in kig/021-slug, 021-slug and feature/021_slug,
132
+ # and 018.01 in kig/018.01-slug. Longest match wins, so a retroactive branch
133
+ # does not silently resolve to its whole-number neighbour.
134
+ if [[ "$BRANCH" =~ (^|/|-|_)([0-9]{3}(\.[0-9]{2})?)($|[-_]) ]]; then
135
+ emit "${BASH_REMATCH[2]}"
136
+ fi
137
+
138
+ # 2. The diff, and only when it is unambiguous.
139
+ if git rev-parse --verify --quiet "$BASE" >/dev/null 2>&1; then
140
+ touched="$(git diff --name-only "$(git merge-base "$BASE" HEAD)"..HEAD -- "$PLANS_DIR" 2>/dev/null \
141
+ | awk -F/ 'NF>2 {print $2}' | grep -oE "^${NUM_RE}" | sort -u || true)"
142
+ count="$(printf '%s' "$touched" | grep -c . || true)"
143
+
144
+ if [[ "$count" == "1" ]]; then
145
+ emit "$touched"
146
+ elif [[ "${count:-0}" -gt 1 ]]; then
147
+ die "plan-number: this branch touches several plans and none is obviously primary:
148
+ $(printf '%s' "$touched" | tr '\n' ' ')
149
+ Name the branch after the one it implements (kig/NNN-slug), or pass the title yourself."
150
+ fi
151
+ fi
152
+
153
+ die "plan-number: cannot resolve a plan for branch '${BRANCH:-<detached>}'.
154
+
155
+ It carries no NNN in its name and its diff touches no single plan folder.
156
+
157
+ - If it implements a plan, rename the branch: git branch -m ${USER}/NNN-slug
158
+ - If it implements no plan, say so explicitly: plan-number --none --title '...'
159
+ gives you [XXX]. Infrastructure, dependency bumps and hotfixes are not
160
+ features, and [XXX] is the right answer for them - but it is your assertion
161
+ to make, not this script's guess.
162
+
163
+ Refusing rather than guessing: pull-requests.md is generated from these titles,
164
+ and a wrong number files the work under a plan that did not do it."
data/bin/setup ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+ IFS=$'\n\t'
5
+ # set -v
6
+
7
+ command -v just>/dev/null || brew install just
8
+ command -v rbenv>/dev/null || brew install rbenv
9
+
10
+ ruby_version="$(cat .ruby-version | tr -d '\n')"
11
+ current_ruby_version="$(ruby -e 'puts RbConfig::CONFIG["RUBY_PROGRAM_VERSION"]')"
12
+
13
+ if [[ ${ruby_version} != ${current_ruby_version} ]]; then
14
+ bash -c "$(curl -fsSL https://bit.ly/ruby-install-0-2-1)" -- ${ruby_version}
15
+ else
16
+ echo -e "\e[0;33m[OK]\e[0m Ruby version ${ruby_version} is installed and current."
17
+ fi
18
+
19
+ rbenv versions | grep -q ${ruby_version} || {
20
+ echo -e "\e[1;31mCan't find ruby version ${ruby_version} after an attempted install.\e[0m"
21
+ exit 1
22
+ }
23
+
24
+ bundle check>/dev/null && {
25
+ echo -e "\e[0;33m[OK]\e[0m Gemfile dependencies are already satisfied."
26
+ } || bundle install -j 12
@@ -0,0 +1,3 @@
1
+ # Plan
2
+
3
+ 1. U1 Reject quantities below 1 in `lib/cart.rb`, with a spec.
@@ -0,0 +1,12 @@
1
+ # Pull Requests
2
+
3
+ | Pull Request Number | Pull Request Name | Status |
4
+ | ------------------: | :---------------- | -----: |
5
+ | 12 | [[001.00] Quantity floor](https://github.com/example/shop/pull/12) | Open 🟡 |
6
+
7
+ Looks good. 👍🏼 to deploy
8
+
9
+ > [!NOTE]
10
+ >
11
+ > [2026-09-04 05:00:00 PM PDT] [ agent: hansolo-reviewer status: Started, round 1 ]
12
+ > [2026-09-04 05:01:02 PM PDT] [ agent: hansolo-reviewer status: Completed, round 1 (approved) ]
@@ -0,0 +1,5 @@
1
+ # Quantity Floor
2
+
3
+ ## Goal
4
+
5
+ `Cart#add` rejects any quantity below 1, zero included.
@@ -0,0 +1,9 @@
1
+ class Cart
2
+ class InvalidQuantity < ArgumentError; end
3
+
4
+ def add(sku, quantity)
5
+ raise InvalidQuantity if quantity.negative?
6
+
7
+ quantity
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ require_relative "../lib/cart"
2
+
3
+ RSpec.describe Cart do
4
+ it "rejects a negative quantity" do
5
+ expect { described_class.new.add("A1", -1) }.to raise_error(Cart::InvalidQuantity)
6
+ end
7
+ end
@@ -0,0 +1 @@
1
+ {"seconds": 62, "tokens": 18000, "state": "in_review"}
@@ -0,0 +1,3 @@
1
+ # Plan
2
+
3
+ 1. U1 Reject quantities below 1 in `lib/cart.rb`, with a spec.
@@ -0,0 +1,15 @@
1
+ # Pull Requests
2
+
3
+ | Pull Request Number | Pull Request Name | Status |
4
+ | ------------------: | :---------------- | -----: |
5
+ | 12 | [[001.00] Quantity floor](https://github.com/example/shop/pull/12) | Open 🟡 |
6
+
7
+ ## Findings
8
+
9
+ 1. `lib/cart.rb:5` rejects only negatives, so `add("A1", 0)` returns 0 instead of raising. The spec says zero included. Failing scenario: `expect { cart.add("A1", 0) }.to raise_error(Cart::InvalidQuantity)`.
10
+ 2. `spec/cart_spec.rb` has no example for zero, which is how the defect got past CI.
11
+
12
+ > [!NOTE]
13
+ >
14
+ > [2026-09-04 05:00:00 PM PDT] [ agent: hansolo-reviewer status: Started, round 1 ]
15
+ > [2026-09-04 05:03:10 PM PDT] [ agent: hansolo-reviewer status: Completed, round 1 (rejected 1/2) ]
@@ -0,0 +1,5 @@
1
+ # Quantity Floor
2
+
3
+ ## Goal
4
+
5
+ `Cart#add` rejects any quantity below 1, zero included.
@@ -0,0 +1,9 @@
1
+ class Cart
2
+ class InvalidQuantity < ArgumentError; end
3
+
4
+ def add(sku, quantity)
5
+ raise InvalidQuantity if quantity.negative?
6
+
7
+ quantity
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ require_relative "../lib/cart"
2
+
3
+ RSpec.describe Cart do
4
+ it "rejects a negative quantity" do
5
+ expect { described_class.new.add("A1", -1) }.to raise_error(Cart::InvalidQuantity)
6
+ end
7
+ end
@@ -0,0 +1 @@
1
+ {"seconds": 190, "tokens": 61000, "state": "in_review"}
@@ -0,0 +1,60 @@
1
+ id: review-medium
2
+ agent: hansolo-reviewer
3
+ depth: medium
4
+ description: >
5
+ A pull request with a seeded defect: the spec asks to reject quantities
6
+ below 1, the code rejects only negatives, so zero slips through and no
7
+ test covers it. hansolo must find it, list it, and sign rejected; an
8
+ approval means the defect was missed. Reviewers write no code.
9
+ fixture:
10
+ state: ready_for_review
11
+ slug: quantity-floor
12
+ files:
13
+ spec.md: |
14
+ # Quantity Floor
15
+
16
+ ## Goal
17
+
18
+ `Cart#add` rejects any quantity below 1, zero included.
19
+ plan.md: |
20
+ # Plan
21
+
22
+ 1. U1 Reject quantities below 1 in `lib/cart.rb`, with a spec.
23
+ pull-requests.md: |
24
+ # Pull Requests
25
+
26
+ | Pull Request Number | Pull Request Name | Status |
27
+ | ------------------: | :---------------- | -----: |
28
+ | 12 | [[001.00] Quantity floor](https://github.com/example/shop/pull/12) | Open 🟡 |
29
+ repo:
30
+ lib/cart.rb: |
31
+ class Cart
32
+ class InvalidQuantity < ArgumentError; end
33
+
34
+ def add(sku, quantity)
35
+ raise InvalidQuantity if quantity.negative?
36
+
37
+ quantity
38
+ end
39
+ end
40
+ spec/cart_spec.rb: |
41
+ require_relative "../lib/cart"
42
+
43
+ RSpec.describe Cart do
44
+ it "rejects a negative quantity" do
45
+ expect { described_class.new.add("A1", -1) }.to raise_error(Cart::InvalidQuantity)
46
+ end
47
+ end
48
+ expect:
49
+ signed: rejected
50
+ files_absent: [rewrite.md]
51
+ sections:
52
+ pull-requests.md: [Findings]
53
+ contains:
54
+ pull-requests.md: [lib/cart.rb, "/[Zz]ero|\\b0\\b/"]
55
+ untouched: ["**/*"]
56
+ depth:
57
+ medium:
58
+ count: { file: pull-requests.md, section: Findings, pattern: '^[ \t]*(?:[-*]|\d+\.)[ \t]', min: 1, max: 6 }
59
+ max_seconds: 300
60
+ max_tokens: 150000
@@ -0,0 +1,9 @@
1
+ # Blocked
2
+
3
+ ## B1 Retention period
4
+
5
+ How long is an abandoned cart kept before it is deleted?
6
+
7
+ ## A1
8
+
9
+ Ninety days, counted from the last change to the cart. Decided by the product owner on 2026-09-01.
@@ -0,0 +1,16 @@
1
+ # Cart Retention
2
+
3
+ Delete abandoned carts automatically.
4
+
5
+ ## Research
6
+
7
+ 1. Carts are never deleted today (`lib/cart_store.rb:9`).
8
+
9
+ ## Constraints
10
+
11
+ - Deletion runs nightly.
12
+
13
+ > [!NOTE]
14
+ >
15
+ > [2026-09-04 06:00:00 PM PDT] [ agent: lando-broker status: Started, round 1 ]
16
+ > [2026-09-04 06:00:19 PM PDT] [ agent: lando-broker status: Completed, round 1 ]
@@ -0,0 +1 @@
1
+ {"seconds": 19, "tokens": 6100, "state": "product_blocked"}
@@ -0,0 +1,18 @@
1
+ # Cart Retention
2
+
3
+ Delete abandoned carts automatically.
4
+
5
+ ## Research
6
+
7
+ 1. Carts are never deleted today (`lib/cart_store.rb:9`).
8
+
9
+ ## Constraints
10
+
11
+ - Deletion runs nightly.
12
+ - An abandoned cart is kept for ninety days, counted from the last change to the cart, then deleted.
13
+ Decided 2026-09-01 by the product owner: retention is ninety days from the last change.
14
+
15
+ > [!NOTE]
16
+ >
17
+ > [2026-09-04 06:00:00 PM PDT] [ agent: lando-broker status: Started, round 1 ]
18
+ > [2026-09-04 06:00:41 PM PDT] [ agent: lando-broker status: Completed, round 1 ]
@@ -0,0 +1 @@
1
+ {"seconds": 41, "tokens": 9800, "state": "researched"}
@@ -0,0 +1,44 @@
1
+ id: unblock-fast
2
+ agent: lando-broker
3
+ depth: fast
4
+ description: >
5
+ One answered product question. lando must fold the answer into spec.md as
6
+ settled fact with a provenance line, delete blocked.md because nothing is
7
+ left open, and not create plan.md. The folder then resyncs out of 🅱️.
8
+ fixture:
9
+ state: product_blocked
10
+ slug: cart-retention
11
+ files:
12
+ spec.md: |
13
+ # Cart Retention
14
+
15
+ Delete abandoned carts automatically.
16
+
17
+ ## Research
18
+
19
+ 1. Carts are never deleted today (`lib/cart_store.rb:9`).
20
+
21
+ ## Constraints
22
+
23
+ - Deletion runs nightly.
24
+ blocked.md: |
25
+ # Blocked
26
+
27
+ ## B1 Retention period
28
+
29
+ How long is an abandoned cart kept before it is deleted?
30
+
31
+ ## A1
32
+
33
+ Ninety days, counted from the last change to the cart. Decided by the product owner on 2026-09-01.
34
+ expect:
35
+ state: researched
36
+ signed: true
37
+ files_absent: [blocked.md, plan.md]
38
+ contains:
39
+ spec.md: ["/[Nn]inety days|90 days/", "Decided "]
40
+ depth:
41
+ fast:
42
+ count: { file: spec.md, pattern: "Decided ", min: 1, max: 1 }
43
+ max_seconds: 120
44
+ max_tokens: 40000
@@ -0,0 +1,26 @@
1
+ # Filing Deadlines
2
+
3
+ Show each small business its next estimated-tax filing deadline, moved for weekends and holidays.
4
+
5
+ ## What research needs to settle
6
+
7
+ 1. Which deadlines apply, and where do they come from?
8
+ 2. How is a deadline moved when it falls on a weekend or holiday?
9
+ 3. Does the codebase already know about holidays?
10
+
11
+ ## Research
12
+
13
+ Four quarterly deadlines apply (https://tax.example.gov/estimated-payments). Weekend dates move to Monday. `lib/calendar.rb` has a holiday list.
14
+
15
+ ### Contradictions
16
+
17
+ None found.
18
+
19
+ ### Could not settle
20
+
21
+ Nothing.
22
+
23
+ > [!NOTE]
24
+ >
25
+ > [2026-09-04 11:29:20 AM PDT] [ agent: leah-researcher status: Started, round 1 ]
26
+ > [2026-09-04 11:30:12 AM PDT] [ agent: leah-researcher status: Completed, round 1 ]
@@ -0,0 +1 @@
1
+ {"seconds": 52, "tokens": 12400, "state": "researched"}