hecks 7.0.0 → 7.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 (257) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/postgres_era/lineage/chain_sql.rb +2 -1
  3. data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +4 -2
  4. data/lib/hecks/adapters/driven/postgres_era/lineage/split_sql.rb +16 -8
  5. data/lib/hecks/adapters/driven/postgres_era/lineage/transform_sql.rb +7 -3
  6. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +2 -1
  7. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/mint_request.rb +2 -1
  8. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/refusals.rb +2 -1
  9. data/lib/hecks/adapters/driving/cli_runner/bang_rule.rb +3 -0
  10. data/lib/hecks/adapters/driving/cli_runner/completion_words.rb +2 -0
  11. data/lib/hecks/adapters/driving/cli_runner/read_context.rb +6 -3
  12. data/lib/hecks/adapters/driving/cli_runner/renamed_names.rb +2 -1
  13. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +2 -2
  14. data/lib/hecks/adapters/driving/cli_runner/resume_words.rb +1 -0
  15. data/lib/hecks/adapters/driving/cli_runner/run_reference.rb +6 -3
  16. data/lib/hecks/adapters/driving/cli_runner/search_words.rb +2 -0
  17. data/lib/hecks/adapters/driving/cli_runner/tour_words.rb +1 -0
  18. data/lib/hecks/adapters/driving/cli_runner/verb_names.rb +2 -1
  19. data/lib/hecks/adapters/driving/cli_runner/verbosity.rb +2 -0
  20. data/lib/hecks/adapters/driving/cli_runner/word_help.rb +40 -0
  21. data/lib/hecks/adapters/driving/cli_runner.rb +2 -0
  22. data/lib/hecks/adapters/driving/mcp/progress_sink.rb +4 -2
  23. data/lib/hecks/adapters/driving/mcp.rb +2 -1
  24. data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +2 -1
  25. data/lib/hecks/adapters/driving/ruby/aggregate_module/hooks.rb +28 -14
  26. data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +4 -2
  27. data/lib/hecks/assembly.rb +18 -4
  28. data/lib/hecks/behaviors/expectations/adapter_selection.rb +82 -0
  29. data/lib/hecks/behaviors/expectations/boot.rb +12 -5
  30. data/lib/hecks/bluebook/acknowledgements.rb +1 -1
  31. data/lib/hecks/bluebook/command.rb +3 -2
  32. data/lib/hecks/bluebook/declaration.rb +2 -1
  33. data/lib/hecks/bluebook/dsl/adapter_builder.rb +9 -1
  34. data/lib/hecks/bluebook/dsl/bluebook_builder/metadata.rb +6 -4
  35. data/lib/hecks/bluebook/dsl/bluebook_builder/modules.rb +6 -3
  36. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/correlation_keys.rb +2 -1
  37. data/lib/hecks/bluebook/dsl/command_builder/folded_sets.rb +4 -2
  38. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +2 -1
  39. data/lib/hecks/bluebook/hecksagon.rb +6 -3
  40. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +9 -2
  41. data/lib/hecks/bluebook/meta_validator.rb +1 -1
  42. data/lib/hecks/bluebook/model_check/aggregate_checks.rb +2 -1
  43. data/lib/hecks/bluebook/model_check/classification_checks.rb +51 -0
  44. data/lib/hecks/bluebook/model_check/context_checks.rb +2 -0
  45. data/lib/hecks/bluebook/model_check/redundant_relationships.rb +2 -1
  46. data/lib/hecks/ci_speed/adapters/shard_partitioner.adapter +7 -0
  47. data/lib/hecks/ci_speed/adapters/shard_partitioner.rb +66 -0
  48. data/lib/hecks/ci_speed/adapters/timings_source.adapter +7 -0
  49. data/lib/hecks/ci_speed/adapters/timings_source.rb +68 -0
  50. data/lib/hecks/ci_speed/ci_speed.bluebook +111 -0
  51. data/lib/hecks/ci_speed/ci_speed.ports.hecksagon +22 -0
  52. data/lib/hecks/ci_workflows/ci_workflows.bluebook +50 -6
  53. data/lib/hecks/coder/coder.bluebook +3 -1
  54. data/lib/hecks/command_names/index.rb +1 -1
  55. data/lib/hecks/command_names/resolution.rb +15 -5
  56. data/lib/hecks/composer/toolbox.rb +2 -1
  57. data/lib/hecks/corpus.rb +4 -0
  58. data/lib/hecks/doc/reference/readme.rb +1 -1
  59. data/lib/hecks/framework_bluebooks.rb +3 -2
  60. data/lib/hecks/fuzzing/differential.rb +1 -1
  61. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  62. data/lib/hecks/fuzzing/isolated_boot/postgres_scratch.rb +1 -1
  63. data/lib/hecks/fuzzing/persistence_parity.rb +1 -1
  64. data/lib/hecks/fuzzing/target_capabilities.rb +1 -1
  65. data/lib/hecks/hecks/adapters/ci_runs.adapter +7 -0
  66. data/lib/hecks/hecks/adapters/ci_runs.rb +80 -0
  67. data/lib/hecks/hecks/adapters/claude_agent_list.rb +43 -0
  68. data/lib/hecks/hecks/adapters/claude_agents.rb +50 -19
  69. data/lib/hecks/hecks/adapters/claude_jobs.rb +36 -0
  70. data/lib/hecks/hecks/adapters/codebase/corpus_queries.rb +2 -1
  71. data/lib/hecks/hecks/adapters/codebase/minitest_runs.rb +4 -2
  72. data/lib/hecks/hecks/adapters/codebase/run_options.rb +2 -1
  73. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +3 -19
  74. data/lib/hecks/hecks/adapters/dispatching/brief_text.rb +5 -3
  75. data/lib/hecks/hecks/adapters/dispatching/launcher.rb +2 -1
  76. data/lib/hecks/hecks/adapters/dispatching/plan.rb +8 -4
  77. data/lib/hecks/hecks/adapters/dispatching/settings.rb +2 -1
  78. data/lib/hecks/hecks/adapters/dispatching/settler.rb +4 -2
  79. data/lib/hecks/hecks/adapters/dispatching.rb +6 -3
  80. data/lib/hecks/hecks/adapters/doctor_reading.rb +4 -2
  81. data/lib/hecks/hecks/adapters/git_landing/carried.rb +42 -0
  82. data/lib/hecks/hecks/adapters/git_landing/finisher.rb +2 -1
  83. data/lib/hecks/hecks/adapters/git_landing/flagged.rb +32 -0
  84. data/lib/hecks/hecks/adapters/git_landing/tidy.rb +27 -14
  85. data/lib/hecks/hecks/adapters/git_landing.rb +30 -13
  86. data/lib/hecks/hecks/adapters/git_leftovers.adapter +7 -0
  87. data/lib/hecks/hecks/adapters/git_leftovers.rb +49 -0
  88. data/lib/hecks/hecks/adapters/git_main_pull.rb +2 -2
  89. data/lib/hecks/hecks/adapters/git_train.rb +9 -5
  90. data/lib/hecks/hecks/adapters/git_worktree_add.rb +8 -4
  91. data/lib/hecks/hecks/adapters/github_pull_requests.rb +2 -1
  92. data/lib/hecks/hecks/adapters/in_process_operations.rb +2 -1
  93. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +1 -1
  94. data/lib/hecks/hecks/adapters/journal_store/readings/composition.rb +2 -1
  95. data/lib/hecks/hecks/adapters/journal_store/translation_write/shape_edge.rb +4 -2
  96. data/lib/hecks/hecks/adapters/journal_store/translation_write.rb +4 -2
  97. data/lib/hecks/hecks/adapters/landed_branches.rb +38 -0
  98. data/lib/hecks/hecks/adapters/main_boarding.adapter +7 -0
  99. data/lib/hecks/hecks/adapters/main_boarding.rb +85 -0
  100. data/lib/hecks/hecks/adapters/main_integration/checks.rb +63 -0
  101. data/lib/hecks/hecks/adapters/main_integration/command_runner.rb +32 -0
  102. data/lib/hecks/hecks/adapters/main_integration/range_guard.rb +49 -0
  103. data/lib/hecks/hecks/adapters/main_integration/repository.rb +83 -0
  104. data/lib/hecks/hecks/adapters/main_integration/runs.rb +51 -0
  105. data/lib/hecks/hecks/adapters/main_integration/train.rb +74 -0
  106. data/lib/hecks/hecks/adapters/main_integration.adapter +7 -0
  107. data/lib/hecks/hecks/adapters/main_integration.rb +77 -0
  108. data/lib/hecks/hecks/adapters/main_verification.adapter +7 -0
  109. data/lib/hecks/hecks/adapters/main_verification.rb +89 -0
  110. data/lib/hecks/hecks/adapters/push_recorder.adapter +7 -0
  111. data/lib/hecks/hecks/adapters/push_recorder.rb +78 -0
  112. data/lib/hecks/hecks/adapters/red_flag_guard.rb +62 -0
  113. data/lib/hecks/hecks/adapters/red_flag_marker.rb +101 -0
  114. data/lib/hecks/hecks/adapters/red_flag_report.rb +59 -0
  115. data/lib/hecks/hecks/adapters/red_flag_survey.rb +65 -0
  116. data/lib/hecks/hecks/adapters/red_flagging.adapter +7 -0
  117. data/lib/hecks/hecks/adapters/red_flagging.rb +98 -0
  118. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +6 -1
  119. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +6 -3
  120. data/lib/hecks/hecks/adapters/settings_reading/event_tap_row.rb +33 -0
  121. data/lib/hecks/hecks/adapters/settings_reading.rb +3 -1
  122. data/lib/hecks/hecks/codebase.behaviors +0 -6
  123. data/lib/hecks/hecks/custodian.behaviors +22 -1
  124. data/lib/hecks/hecks/delegate/delegate.bluebook +128 -15
  125. data/lib/hecks/hecks/deliver/deliver.bluebook +450 -2
  126. data/lib/hecks/hecks/environments/memory.world +5 -0
  127. data/lib/hecks/hecks/hecks.contextmap +8 -0
  128. data/lib/hecks/hecks/hecks.hecksagon +106 -6
  129. data/lib/hecks/hecks/hecks.world +9 -1
  130. data/lib/hecks/hecks/migrate/migrate.bluebook +6 -1
  131. data/lib/hecks/hecks/redflag/redflag.bluebook +249 -0
  132. data/lib/hecks/hecks/test/test.bluebook +0 -19
  133. data/lib/hecks/language/adapter.bluebook +13 -0
  134. data/lib/hecks/language/bluebook/vocabulary.bluebook +8 -9
  135. data/lib/hecks/language/sentence/sentence.bluebook +2 -0
  136. data/lib/hecks/pipeline/pipeline.bluebook +251 -0
  137. data/lib/hecks/pipeline/pipeline.hecksagon +17 -0
  138. data/lib/hecks/pipeline_board/adapters/cache.rb +43 -0
  139. data/lib/hecks/pipeline_board/adapters/pipeline_fill.rb +56 -0
  140. data/lib/hecks/pipeline_board/adapters/pipeline_git.rb +82 -0
  141. data/lib/hecks/pipeline_board/adapters/pipeline_github/runs.rb +66 -0
  142. data/lib/hecks/pipeline_board/adapters/pipeline_github.adapter +7 -0
  143. data/lib/hecks/pipeline_board/adapters/pipeline_github.rb +70 -0
  144. data/lib/hecks/pipeline_board/adapters/pipeline_jobs.adapter +7 -0
  145. data/lib/hecks/pipeline_board/adapters/pipeline_jobs.rb +87 -0
  146. data/lib/hecks/pipeline_board/adapters/pipeline_journal/chain.rb +40 -0
  147. data/lib/hecks/pipeline_board/adapters/pipeline_journal/tap.rb +75 -0
  148. data/lib/hecks/pipeline_board/adapters/pipeline_journal.adapter +7 -0
  149. data/lib/hecks/pipeline_board/adapters/pipeline_journal.rb +83 -0
  150. data/lib/hecks/pipeline_board/adapters/pipeline_stages/ci_tiles.rb +85 -0
  151. data/lib/hecks/pipeline_board/adapters/pipeline_stages/fanout.rb +73 -0
  152. data/lib/hecks/pipeline_board/adapters/pipeline_stages/gather.rb +57 -0
  153. data/lib/hecks/pipeline_board/adapters/pipeline_stages/metrics.rb +36 -0
  154. data/lib/hecks/pipeline_board/adapters/pipeline_stages/strip.rb +102 -0
  155. data/lib/hecks/pipeline_board/adapters/pipeline_stages/tile_kit.rb +57 -0
  156. data/lib/hecks/pipeline_board/adapters/pipeline_stages/work_tiles.rb +59 -0
  157. data/lib/hecks/pipeline_board/adapters/pipeline_stages.adapter +7 -0
  158. data/lib/hecks/pipeline_board/adapters/pipeline_stages.rb +84 -0
  159. data/lib/hecks/pipeline_board/adapters/pipeline_tickets.adapter +7 -0
  160. data/lib/hecks/pipeline_board/adapters/pipeline_tickets.rb +77 -0
  161. data/lib/hecks/pipeline_board/adapters/pipeline_web/page.html +292 -0
  162. data/lib/hecks/pipeline_board/adapters/pipeline_web/sequence.html +190 -0
  163. data/lib/hecks/pipeline_board/adapters/pipeline_web/server.rb +105 -0
  164. data/lib/hecks/pipeline_board/adapters/pipeline_web/snapshot.rb +57 -0
  165. data/lib/hecks/pipeline_board/adapters/pipeline_web/stream.rb +59 -0
  166. data/lib/hecks/pipeline_board/adapters/pipeline_web/wiring.html +536 -0
  167. data/lib/hecks/pipeline_board/adapters/pipeline_web.adapter +7 -0
  168. data/lib/hecks/pipeline_board/adapters/pipeline_web.rb +80 -0
  169. data/lib/hecks/pipeline_board/adapters/pipeline_wiring/details.rb +85 -0
  170. data/lib/hecks/pipeline_board/adapters/pipeline_wiring/graph.rb +120 -0
  171. data/lib/hecks/pipeline_board/adapters/pipeline_wiring.adapter +7 -0
  172. data/lib/hecks/pipeline_board/adapters/pipeline_wiring.rb +36 -0
  173. data/lib/hecks/pipeline_board/adapters/probe.rb +48 -0
  174. data/lib/hecks/pipeline_board/pipeline_board.bluebook +144 -0
  175. data/lib/hecks/pipeline_board/pipeline_board.hecksagon +12 -0
  176. data/lib/hecks/pipeline_board/pipeline_board.ports.hecksagon +47 -0
  177. data/lib/hecks/ports/persistence/binding_policy.rb +2 -1
  178. data/lib/hecks/ports/persistence/plugins/era/translation/compose.rb +4 -3
  179. data/lib/hecks/ports/persistence/repository_factory.rb +2 -1
  180. data/lib/hecks/projections/ci_pipeline/table.rb +2 -1
  181. data/lib/hecks/projections/deploy/lambda/stack/recipes.rb +2 -2
  182. data/lib/hecks/projections/undo.rb +2 -1
  183. data/lib/hecks/projector/cli_projector/command_help.rb +5 -4
  184. data/lib/hecks/projector/cli_projector/renames.rb +4 -2
  185. data/lib/hecks/projector/cli_projector/usage.rb +4 -3
  186. data/lib/hecks/projector/exporter/adapters.rb +3 -2
  187. data/lib/hecks/projector/narrate_projector/use_cases.rb +74 -0
  188. data/lib/hecks/projector/narrate_projector.rb +10 -3
  189. data/lib/hecks/quality_control/adapters/main_maintenance.rb +9 -5
  190. data/lib/hecks/quality_control/adapters/qa_auto_mint.rb +21 -2
  191. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +1 -1
  192. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +2 -1
  193. data/lib/hecks/quality_control/cli/qa_fix_main_bug/publishing.rb +8 -4
  194. data/lib/hecks/quality_control/cli/qa_fix_main_bug.rb +10 -8
  195. data/lib/hecks/quality_control/cli/qa_generated_domains/promotion.rb +2 -2
  196. data/lib/hecks/quality_control/cli/qa_main_gate/gate_run.rb +6 -3
  197. data/lib/hecks/quality_control/cli/qa_main_gate/logging.rb +8 -4
  198. data/lib/hecks/quality_control/cli/qa_main_gate/shell.rb +4 -2
  199. data/lib/hecks/quality_control/cli/qa_main_gate/sweeping.rb +8 -4
  200. data/lib/hecks/quality_control/cli/qa_main_gate/worktree.rb +3 -2
  201. data/lib/hecks/quality_control/cli/qa_main_gate.rb +10 -5
  202. data/lib/hecks/quality_control/cli/qa_sweep/all_mode/children.rb +1 -1
  203. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +1 -1
  204. data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
  205. data/lib/hecks/quality_control/cli/qa_tick.rb +6 -3
  206. data/lib/hecks/quality_control/quality_control.bluebook +11 -11
  207. data/lib/hecks/quality_control/quality_control.ports.hecksagon +6 -3
  208. data/lib/hecks/query_ir/touchpoints.rb +2 -1
  209. data/lib/hecks/refused.rb +2 -1
  210. data/lib/hecks/release/approval.rb +4 -2
  211. data/lib/hecks/runtime/event_log.rb +27 -0
  212. data/lib/hecks/runtime/event_tap.rb +106 -0
  213. data/lib/hecks/runtime/loader/boot_steps.rb +1 -1
  214. data/lib/hecks/runtime/loader.rb +35 -19
  215. data/lib/hecks/runtime/registry/durability_warnings.rb +12 -5
  216. data/lib/hecks/runtime/registry/runtime_state.rb +4 -1
  217. data/lib/hecks/runtime/registry/saga_anchor.rb +54 -0
  218. data/lib/hecks/runtime/registry/saga_durability_rule.rb +66 -0
  219. data/lib/hecks/runtime/registry/saga_persistence.rb +58 -12
  220. data/lib/hecks/runtime/registry/verification.rb +7 -3
  221. data/lib/hecks/runtime/saga_interpreter/lifecycle.rb +2 -1
  222. data/lib/hecks/runtime/saga_interpreter.rb +1 -1
  223. data/lib/hecks/rust_format.rb +2 -1
  224. data/lib/hecks/shape_ledger/edge_hygiene.rb +10 -5
  225. data/lib/hecks/shape_ledger.rb +1 -1
  226. data/lib/hecks/storehouse/audit.rb +2 -1
  227. data/lib/hecks/storehouse/identity.rb +6 -3
  228. data/lib/hecks/storehouse.rb +2 -1
  229. data/lib/hecks/tickets/bluebook/tickets.bluebook +5 -4
  230. data/lib/hecks/tools/ci_workflows.rb +2 -1
  231. data/lib/hecks/tools/document.rb +12 -1
  232. data/lib/hecks/tools/downstream_model_check.rb +4 -2
  233. data/lib/hecks/tools/fuzz_sweep/shards.rb +9 -30
  234. data/lib/hecks/tools/model_check/examination.rb +25 -2
  235. data/lib/hecks/tools/move_disables.rb +4 -2
  236. data/lib/hecks/tools/regeneration_run/scratch.rb +2 -1
  237. data/lib/hecks/tools/rspec_shard_files.rb +12 -15
  238. data/lib/hecks/tools/sequence_script.rb +1 -1
  239. data/lib/hecks/tools/shard_planner.rb +65 -0
  240. data/lib/hecks/tools/stage_progress.rb +2 -1
  241. data/lib/hecks/tools/stress_concurrency_specs.rb +2 -1
  242. data/lib/hecks/version.rb +1 -1
  243. data/lib/hecks/vocabulary.rb +8 -9
  244. data/lib/hecks.rb +5 -2
  245. data/prebuilt/{13c04bdc5970295b310f0a980d397ba722232c8da99cc1a0cdb383249921e871.marshal → 4b81646f769ae5b1e7457f9b11c48d8bc1093e96a1dd6fb61864065121ca6f01.marshal} +0 -0
  246. data/prebuilt/{e3857b77f47d9ecc1787b2e1f5136364f3cb3444ef4aea5479f3b98167affea2.marshal → 681890632e3f4a99646623dcae44af36b1bad66c515f5abf67a2460372b9c6fd.marshal} +0 -0
  247. data/rust/host/HECKS_RELEASE +1 -1
  248. data/rust/parser/src/emit.rs +5 -1
  249. data/rust/parser/src/ir.rs +2 -0
  250. data/rust/parser/src/keywords.rs +12 -0
  251. data/rust/parser/src/main.rs +1 -0
  252. data/rust/parser/src/parse/adapter.rs +1 -0
  253. data/rust/parser/src/phrases.rs +16 -0
  254. data/rust/parser/src/sentence/examples.rs +93 -0
  255. data/rust/parser/src/sentence/mod.rs +1 -0
  256. metadata +89 -5
  257. data/lib/hecks/tools/refresh_rspec_runtime_baseline.rb +0 -166
@@ -5,16 +5,18 @@ require_relative "shell"
5
5
  require "time"
6
6
  require_relative "console_capture"
7
7
  require_relative "claude_transcript"
8
+ require_relative "claude_jobs"
9
+ require_relative "claude_agent_list"
8
10
  require_relative "dispatching/settings"
9
11
 
10
12
  module Hecks
11
13
  module Adapters
12
- # The `Agents` port's adapter for `AgentRun`: finds the directory a session will work in, starts
13
- # a background Claude session there with the `claude` command, and reports the state a started
14
- # session says it is in.
14
+ # The `Agents` port's adapter for `AgentRun`: finds the directory a session will work in and the
15
+ # agents `claude` lists there, starts a background Claude session with the `claude` command,
16
+ # and reads the state its job is in.
15
17
  #
16
- # It decides nothing. Whether a brief is enough, whether an agent name is acceptable and which
17
- # states mean a session has ended are the bluebook's; this only looks, starts and reads. The
18
+ # It decides nothing. Whether a brief is enough, whether an agent is listed and which states
19
+ # are new or mean a session has ended are the bluebook's; this only looks, starts and reads. The
18
20
  # command is an argument list, never a string for a shell, and a `--` keeps a brief from being
19
21
  # taken for a flag.
20
22
  class ClaudeAgents
@@ -24,10 +26,14 @@ module Hecks
24
26
  # A background start prints `backgrounded · <id>` first, then how to attach to the session.
25
27
  SESSION_ID = /\Abackgrounded\W+([0-9A-Za-z][0-9A-Za-z_-]{5,})/
26
28
 
29
+ # How long a just-started session is given to fail before its job is read, in seconds.
30
+ SETTLE_SECONDS = 3
31
+
27
32
  # Keeps the command to run, so a spec can point it at a stand-in.
28
33
  #
29
34
  # @param aggregate [Object, nil] unused
30
- # @param settings [Hash] `command:` the program that starts sessions (default `claude`)
35
+ # @param settings [Hash] `command:` the program that starts sessions (default `claude`),
36
+ # `jobs:` the jobs directory, `settle:` the seconds a new session is given to fail
31
37
  # @param root [String, nil] unused
32
38
  # @param registry [Runtime::Registry, nil] whose Tickets world names the session environment
33
39
  def initialize(aggregate: nil, settings: {}, root: nil, registry: nil)
@@ -35,18 +41,24 @@ module Hecks
35
41
  @session = Dispatching::Settings.new(registry)
36
42
  @shell = Shell.new
37
43
  @transcript = ClaudeTranscript.new(**settings.slice(:projects))
44
+ @jobs = ClaudeJobs.new(directory: settings[:jobs])
45
+ @agents = ClaudeAgentList.new(@command, shell: @shell)
46
+ @settle = settings.fetch(:settle, SETTLE_SECONDS)
38
47
  end
39
48
 
40
- # Says the directory a session would work in is there.
49
+ # Says the directory a session would work in is there, and whether the `claude` there lists
50
+ # the named agent. With no agent named the session runs `claude`'s default, so nothing is
51
+ # asked.
41
52
  #
42
- # @param held [Hash] the `AgentRun` record: `repo`
43
- # @return [Hash{Symbol => Hash}] `report:` that the directory is there
44
- # @raise [ConsoleCapture::Failure] when it is not a directory
53
+ # @param held [Hash] the `AgentRun` record: `repo`, and `agent` when one is named
54
+ # @return [Hash{Symbol => Object}] `report:` that the directory is there, `listed:` whether
55
+ # the agent is listed, and `note:` that in words, naming the agents that are
56
+ # @raise [ConsoleCapture::Failure] when it is not a directory, or `claude` names no agents
45
57
  def locate(**held)
46
58
  repo = plain(held[:repo])
47
59
  raise ConsoleCapture::Failure, "#{repo} is not a directory" unless File.directory?(repo.to_s)
48
60
 
49
- { report: { value: "#{repo} is a directory" } }
61
+ { report: { value: "#{repo} is a directory" }, **agent_listing(repo, plain(held[:agent])) }
50
62
  end
51
63
 
52
64
  # Starts a background session in the directory, with the brief as its first prompt.
@@ -64,15 +76,27 @@ module Hecks
64
76
  { session: { value: id } }
65
77
  end
66
78
 
67
- # Reads the state a started session reports.
79
+ # Reads the state a started session's job is in, beside the state the run last recorded.
68
80
  #
69
- # @param held [Hash] the `AgentRun` record: `session`
70
- # @return [Hash{Symbol => String}] `condition:` the state the session reports, as a bare
71
- # string
72
- # @raise [ConsoleCapture::Failure] when the sessions cannot be listed or this one is not among
73
- # them
74
- def examine(**held)
75
- { condition: listed(plain(held[:session]))["state"].to_s }
81
+ # @param held [Hash] the `AgentRun` record: `session`, `repo`, and the `condition` it holds
82
+ # @return [Hash{Symbol => String}] `state:` and `detail:` from the job, `previous:` the held
83
+ # condition (empty when none), and `checkout:` the run's directory with its trailing slash
84
+ # @raise [ConsoleCapture::Failure] when the job's state cannot be read
85
+ def read_job(**held)
86
+ job = @jobs.state(plain(held[:session]))
87
+ { state: job["state"].to_s, detail: job["detail"].to_s, previous: plain(held[:condition]).to_s,
88
+ checkout: File.join(plain(held[:repo]).to_s, "") }
89
+ end
90
+
91
+ # Waits the settling time, then reads the job as {#read_job} does: a session that dies at
92
+ # once has reported it by then.
93
+ #
94
+ # @param held [Hash] the `AgentRun` record, as for {#read_job}
95
+ # @return [Hash{Symbol => String}] what {#read_job} answers
96
+ # @raise [ConsoleCapture::Failure] when the job's state cannot be read
97
+ def await_job(**held)
98
+ sleep(@settle) if @settle.positive?
99
+ read_job(**held)
76
100
  end
77
101
 
78
102
  # Says whether a person replied in a session after its brief.
@@ -103,6 +127,13 @@ module Hecks
103
127
 
104
128
  private
105
129
 
130
+ def agent_listing(repo, agent)
131
+ return { listed: true, note: "no agent named: the session runs the default agent" } unless agent
132
+
133
+ names = @agents.names(repo)
134
+ { listed: names.include?(agent), note: "agent #{agent} is not available in #{repo}; available: #{names.join(", ")}" }
135
+ end
136
+
106
137
  def listed(id)
107
138
  listing = @shell.capture(@command, "agents", "--json", "--all")
108
139
  raise ConsoleCapture::Failure, "#{@command} could not list sessions: #{listing.err.strip}" unless listing.ok?
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "console_capture"
5
+
6
+ module Hecks
7
+ module Adapters
8
+ # The state a background session's job keeps in `~/.claude/jobs/<session>/state.json`: the
9
+ # `state` it is in (working, blocked, done, failed, stopped) and the `detail` it gave, such as
10
+ # the question a blocked session waits on or why a failed one stopped.
11
+ #
12
+ # It reads and judges nothing: which states are new, and which mean a session ended, are the
13
+ # bluebook's.
14
+ class ClaudeJobs
15
+ # Where the jobs are kept unless a setting names another directory.
16
+ DIRECTORY = "~/.claude/jobs"
17
+
18
+ # @param directory [String, nil] the jobs directory (default `~/.claude/jobs`)
19
+ def initialize(directory: nil)
20
+ @directory = File.expand_path(directory || DIRECTORY)
21
+ end
22
+
23
+ # @param session [String] the session's id, which names its job's directory
24
+ # @return [Hash{String => Object}] the job's state file, parsed
25
+ # @raise [ConsoleCapture::Failure] when the session names no job, or its state cannot be read
26
+ def state(session)
27
+ path = File.join(@directory, session.to_s, "state.json")
28
+ raise ConsoleCapture::Failure, "no job state at #{path}" if session.to_s.empty? || !File.file?(path)
29
+
30
+ JSON.parse(File.read(path))
31
+ rescue JSON::ParserError => e
32
+ raise ConsoleCapture::Failure, "the job state at #{path} cannot be read: #{e.message}"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -3,7 +3,8 @@
3
3
  module Hecks
4
4
  module Adapters
5
5
  class SourceTree
6
- # The `SourceTree` queries that read the corpus of domains: which have Rust, the order regeneration
6
+ # The `SourceTree` queries that read the corpus of domains: which have Rust, the order
7
+ # regeneration
7
8
  # walks them in, and what the IR and its propagation touchpoints say. Each is answered through
8
9
  # `corpus`, which the includer provides.
9
10
  module CorpusQueries
@@ -5,7 +5,8 @@ require "open3"
5
5
  module Hecks
6
6
  module Adapters
7
7
  module Codebase
8
- # The minitest files of a spec directory. RSpec does not run them, so each one runs in its own Ruby
8
+ # The minitest files of a spec directory. RSpec does not run them, so each one runs in its own
9
+ # Ruby
9
10
  # process, and its run summary is reported beside the RSpec output.
10
11
  module MinitestRuns
11
12
  # The summary line minitest prints, such as "3 runs, 5 assertions".
@@ -19,7 +20,8 @@ module Hecks
19
20
  Dir.glob("#{TestSuite::SPEC_DIRECTORY}/**/*_test.rb", base: tree.root).sort
20
21
  end
21
22
 
22
- # Runs each file in its own Ruby process, from the folder, outside this process's bundle so the
23
+ # Runs each file in its own Ruby process, from the folder, outside this process's bundle so
24
+ # the
23
25
  # folder's own gems are the ones it loads.
24
26
  #
25
27
  # @param tree [Tree] the folder the files run in
@@ -3,7 +3,8 @@
3
3
  module Hecks
4
4
  module Adapters
5
5
  module Codebase
6
- # What a spec run's arguments become: the variables the specs read, and the flags a stress run takes.
6
+ # What a spec run's arguments become: the variables the specs read, and the flags a stress run
7
+ # takes.
7
8
  # A person names them in a request, so they never set them in a shell.
8
9
  module RunOptions
9
10
  module_function
@@ -20,11 +20,11 @@ module Hecks
20
20
  # The tooling owns its options, so each ask runs one of the `Hecks::Tools` commands in this
21
21
  # process from the checkout's root, except a single example (the `TestRunner` adapter runs it)
22
22
  # and the persistence fixtures (the `SqliteFixture` adapter). A listing is a pure read.
23
- # Refreshing the committed timings, regenerating the fixtures and writing the committed spec
24
- # list change tracked files, so unconfirmed they report what they would do and write nothing.
23
+ # Regenerating the fixtures and writing the committed spec list change tracked files, so
24
+ # unconfirmed they report what they would do and write nothing.
25
25
  module TestSuite
26
26
  # Every operation this family carries out.
27
- OPERATIONS = %w[refresh_runtime_baseline run_spec_example run_spec_files run_impacted_specs run_suite run_changed
27
+ OPERATIONS = %w[run_spec_example run_spec_files run_impacted_specs run_suite run_changed
28
28
  stress_concurrency run_cargo_tests clear_caches regenerate_legacy_fixtures seed_semantics_corpus
29
29
  write_io_parallel_spec_list].freeze
30
30
 
@@ -38,9 +38,6 @@ module Hecks
38
38
  # The tag filter the postgres job lists its specs by when none is named.
39
39
  DEFAULT_TAGS = "--tag io --tag ~fuzzing"
40
40
 
41
- # The committed files a baseline refresh writes.
42
- BASELINES = %w[.github/rspec_runtime_baseline.log .github/postgres_io_runtime_baseline.log].freeze
43
-
44
41
  module_function
45
42
 
46
43
  # Loads the command a `Hecks::Tools` file defines, the first time an ask needs it: several
@@ -79,7 +76,6 @@ module Hecks
79
76
  # @return [String] what the tool printed
80
77
  def tooling(operation, args, tree, shell)
81
78
  case operation
82
- when "refresh_runtime_baseline" then refresh(args, tree)
83
79
  when "stress_concurrency" then stress(args, tree)
84
80
  when "regenerate_legacy_fixtures" then regenerate_fixtures(args, tree, shell)
85
81
  when "seed_semantics_corpus" then seed(args, tree)
@@ -204,18 +200,6 @@ module Hecks
204
200
  # @return [Array<String>] the tag filter, one word each
205
201
  def tag_words(args) = (args[:tags] || DEFAULT_TAGS).split
206
202
 
207
- # @param args [Hash] `workers` and `from_run`
208
- # @param tree [Tree] the checkout whose baselines are rewritten
209
- # @return [String] what was written, or (unconfirmed) what would be
210
- # @raise [ConsoleCapture::Failure] when the refresh ends badly
211
- def refresh(args, tree)
212
- source = args[:from_run] ? ["--from-run", args[:from_run]] : [*args[:workers]&.to_s]
213
- return answer { command("refresh_rspec_runtime_baseline").call(source, root: tree.root) } if args[:confirm] == true
214
-
215
- "dry run, would #{args[:from_run] ? "read CI run #{args[:from_run]}'s timings" : "time a local run"} " \
216
- "and rewrite #{BASELINES.join(", ")} (add --confirm)"
217
- end
218
-
219
203
  # @param args [Hash] `exclude`, `tags`, `write` and `confirm`
220
204
  # @param tree [Tree] the checkout whose spec list is written
221
205
  # @return [String] what was written, or (unconfirmed) how many files would be
@@ -3,12 +3,14 @@
3
3
  module Hecks
4
4
  module Adapters
5
5
  class Dispatching
6
- # The words a session is started with, laid out from a composed brief. Every sentence in it is the
7
- # Coder bluebook's; this only orders the sections, fills in the row's key and lists the areas the
6
+ # The words a session is started with, laid out from a composed brief. Every sentence in it is
7
+ # the
8
+ # Coder bluebook's; this only orders the sections, fills in the row's key and lists the areas
9
+ # the
8
10
  # caller knows.
9
11
  class BriefText
10
12
  # The sections of a brief after the outcome, in the order they are read.
11
- SECTIONS = %w[rules verification workflow landing restraints report].freeze
13
+ SECTIONS = %w[rules review verification workflow landing restraints report].freeze
12
14
 
13
15
  # @param brief [Hash] the composed brief: its attribute values by name
14
16
  def initialize(brief)
@@ -20,7 +20,8 @@ module Hecks
20
20
  # The JSON object a command prints after any log lines.
21
21
  JSON_OBJECT = /\{.*\}/m
22
22
 
23
- # @param repo [String] the checkout whose `exe/hecks` runs the commands and where sessions work
23
+ # @param repo [String] the checkout whose `exe/hecks` runs the commands and where sessions
24
+ # work
24
25
  # @param runner [#call, nil] runs an argument list in `repo` and answers a {Shell::Result}
25
26
  def initialize(repo, runner: nil)
26
27
  @repo = repo
@@ -5,11 +5,14 @@ require_relative "../area_overlap"
5
5
  module Hecks
6
6
  module Adapters
7
7
  class Dispatching
8
- # Which planned rows start a session now, and why each of the others waits. It decides from the
8
+ # Which planned rows start a session now, and why each of the others waits. It decides from
9
+ # the
9
10
  # rows it is given and starts nothing.
10
11
  #
11
- # A planned row waits when it sits in a frozen area, when its area overlaps a started row's or an
12
- # earlier chosen row's (by {AreaOverlap}), or when the machine is loaded or the cap is reached,
12
+ # A planned row waits when it sits in a frozen area, when its area overlaps a started row's or
13
+ # an
14
+ # earlier chosen row's (by {AreaOverlap}), or when the machine is loaded or the cap is
15
+ # reached,
13
16
  # which also hold back every row after it.
14
17
  class Plan
15
18
  # A row, as {Rows} reads it.
@@ -38,7 +41,8 @@ module Hecks
38
41
  planned.each { |row| consider(row) }
39
42
  end
40
43
 
41
- # @return [Array<Row>] the started rows and the chosen ones: whose areas are not to be edited
44
+ # @return [Array<Row>] the started rows and the chosen ones: whose areas are not to be
45
+ # edited
42
46
  def holders = @started + @chosen
43
47
 
44
48
  private
@@ -53,7 +53,8 @@ module Hecks
53
53
  # @return [Integer] the CPU count
54
54
  attr_reader :cpus
55
55
 
56
- # @return [Float] the load above which no session is started: the world's, else the CPU count
56
+ # @return [Float] the load above which no session is started: the world's, else the CPU
57
+ # count
57
58
  def max_load = (declared(MAX_LOAD)[:limit] || @cpus).to_f
58
59
 
59
60
  # @return [Integer] the cap on started rows
@@ -3,8 +3,10 @@
3
3
  module Hecks
4
4
  module Adapters
5
5
  class Dispatching
6
- # Settles the started rows whose sessions have ended: asks each running session how it is, then
7
- # finishes the row of one that ended `done` and yields the row of one that ended any other way,
6
+ # Settles the started rows whose sessions have ended: asks each running session how it is,
7
+ # then
8
+ # finishes the row of one that ended `done` and yields the row of one that ended any other
9
+ # way,
8
10
  # with the condition as the reason. A session still working is left alone.
9
11
  class Settler
10
12
  # @param rows [Rows] where the started rows and the runs are read
@@ -9,7 +9,8 @@ require_relative "dispatching/settler"
9
9
 
10
10
  module Hecks
11
11
  module Adapters
12
- # The `Dispatching` port's adapter for `Dispatch`: reads the Work queue and the machine, says which
12
+ # The `Dispatching` port's adapter for `Dispatch`: reads the Work queue and the machine, says
13
+ # which
13
14
  # planned rows would start a session and why each other waits, and with `confirm` starts them.
14
15
  #
15
16
  # It decides only what the Plan rules decide; whether a row may start is still the bluebook's
@@ -18,7 +19,8 @@ module Hecks
18
19
  class Dispatching
19
20
  # @param registry [Runtime::Registry, nil] the booted registry whose rows are read
20
21
  # @param aggregate [Object, nil] unused
21
- # @param settings [Hash] `load:`, `cpus:` and `runner:` stand in for the machine and the launcher
22
+ # @param settings [Hash] `load:`, `cpus:` and `runner:` stand in for the machine and the
23
+ # launcher
22
24
  # @param root [String, nil] unused
23
25
  def initialize(registry: nil, aggregate: nil, settings: {}, root: nil)
24
26
  @registry = registry
@@ -28,7 +30,8 @@ module Hecks
28
30
  # Says which planned rows start now, and starts them when confirmed.
29
31
  #
30
32
  # @param held [Hash] the `Dispatch` record: `repo`, `budget_minutes`, `confirm`
31
- # @return [Hash{Symbol => Hash}] `report:` the load, the rows started or that would be, and the
33
+ # @return [Hash{Symbol => Hash}] `report:` the load, the rows started or that would be, and
34
+ # the
32
35
  # rows left waiting with the reason
33
36
  def choose(**held)
34
37
  plan = plan_for
@@ -50,7 +50,8 @@ module Hecks
50
50
  [directory, Dir.pwd].uniq.flat_map { |where| Dir[File.join(where, "*.local.{world,hecksagon}")] }.uniq
51
51
  end
52
52
 
53
- # The first line of a compile error, or nil when the file is valid Ruby. Compiling never runs it.
53
+ # The first line of a compile error, or nil when the file is valid Ruby. Compiling never runs
54
+ # it.
54
55
  def syntax_error(path)
55
56
  RubyVM::InstructionSequence.compile(File.read(path), path)
56
57
  nil
@@ -124,7 +125,8 @@ module Hecks
124
125
  row("store", :fail, "#{adapter} directory #{dir} is missing", "mkdir -p #{dir}")
125
126
  end
126
127
 
127
- # Postgres reachable, and the role the URL names exists; a read of the catalog, nothing written.
128
+ # Postgres reachable, and the role the URL names exists; a read of the catalog, nothing
129
+ # written.
128
130
  def reach(url)
129
131
  require "pg"
130
132
  PG.connect(url, connect_timeout: 3) { |conn| role_row(conn, URI.parse(url).user) }
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hecks
4
+ module Adapters
5
+ class GitLanding
6
+ # How a branch stands against the branch it was cut for: whether `onto` carries its commits,
7
+ # by
8
+ # id or by patch, and what a discard says it dropped.
9
+ #
10
+ # Mixed into `Tidy`, which supplies `@onto`, `@discarding`, `@checkout`, `count`, `ancestor?`
11
+ # and
12
+ # `read`.
13
+ module Carried
14
+ private
15
+
16
+ def unmerged(tip)
17
+ return "it has no commit to verify" unless tip
18
+ return if @discarding
19
+
20
+ local = "refs/heads/#{@onto}"
21
+ return unless count("#{local}..#{tip}").positive? || !ancestor?(tip, local)
22
+
23
+ lacking = unmatched(local, tip)
24
+ "it has #{lacking} commit(s) not on #{@onto}" if lacking.positive?
25
+ end
26
+
27
+ # How many commits the branch adds that `onto` carries under no id and no patch: `git cherry`
28
+ # marks one `onto` has a patch-equivalent of with `-`, and one it lacks with `+`.
29
+ def unmatched(local, tip)
30
+ read("cherry", local, tip, dir: @checkout).lines.count { |line| line.start_with?("+") }
31
+ end
32
+
33
+ # What a discard says it dropped, so the commits can be found again by their tip.
34
+ def discarded_note(tip)
35
+ return unless @discarding
36
+
37
+ "it was at #{tip} with #{unmatched("refs/heads/#{@onto}", tip)} commit(s) not on #{@onto}"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -7,7 +7,8 @@ require_relative "../shell"
7
7
  module Hecks
8
8
  module Adapters
9
9
  class GitLanding
10
- # The steps before a landing that an agent used to run by hand: refuse unsaved work, rebase the
10
+ # The steps before a landing that an agent used to run by hand: refuse unsaved work, rebase
11
+ # the
11
12
  # branch onto local `main`, and run the quick gate in the branch's worktree.
12
13
  #
13
14
  # Each step refuses by name at the first failure, with the reason, and leaves the branch as it
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../red_flag_guard"
4
+
5
+ module Hecks
6
+ module Adapters
7
+ class GitLanding
8
+ # What landing on main asks beyond any other landing: a base that is not behind origin, and
9
+ # no standing red flag unless the branch is a red row's fix. A fix that lands records its tip
10
+ # on the flag so the push train may board it. The including class answers `common_git_dir`,
11
+ # `@git` and `refuse_stale_base`.
12
+ module Flagged
13
+ private
14
+
15
+ # @return [Object] what the block answers, run only after main's checks pass
16
+ def on_main(branch, onto)
17
+ return yield unless onto == "main"
18
+
19
+ refuse_stale_base(branch)
20
+ git_dir = common_git_dir
21
+ guard = RedFlagGuard.new(git_dir)
22
+ guard.landing!(branch)
23
+ yield.tap { guard.landed(branch, tip(git_dir)) }
24
+ end
25
+
26
+ # Main's commit, read against the git directory: the worktree a finish lands from is gone
27
+ # by now.
28
+ def tip(git_dir) = @git.capture("--git-dir=#{git_dir}", "rev-parse", "main").out.strip
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../git_worktrees"
4
+ require_relative "carried"
4
5
 
5
6
  module Hecks
6
7
  module Adapters
@@ -10,11 +11,14 @@ module Hecks
10
11
  # When in doubt it keeps both. It removes them only when the worktree is a linked one that
11
12
  # nobody stands in, holds no file that is not tracked and clean (ignored files and build
12
13
  # output count, so a local `.env` is never lost), and its branch tip, verified once, is
13
- # contained in local `onto`. The worktree goes without `--force`, and the branch goes only
14
- # while it still names the verified tip, so a commit made in between is never dropped.
14
+ # contained in local `onto` or every commit it adds has a patch-equivalent there (a rebase or
15
+ # a cherry-pick carries a commit under another id). The worktree goes without `--force`, and
16
+ # the branch goes only while it still names the verified tip, so a commit made in between is
17
+ # never dropped.
15
18
  # `main`, `stable`, `edge`, the target and a branch another worktree holds are never deleted.
16
19
  class Tidy
17
20
  include GitWorktrees
21
+ include Carried
18
22
 
19
23
  # Branches no tidy deletes, whatever they contain.
20
24
  PROTECTED = %w[main stable edge].freeze
@@ -26,14 +30,17 @@ module Hecks
26
30
  # @param checkout [String] the repository's top level
27
31
  # @param branch [String] the landed branch
28
32
  # @param onto [String] the branch it was landed on
29
- # @param retiring [Boolean] whether `checkout` is the very worktree to retire
30
- def initialize(git, checkout, branch:, onto:, retiring: false)
33
+ # @param mode [Symbol] `:land` tidies after a landing, `:retire` removes the worktree named
34
+ # by `checkout`, `:discard` does that although `onto` lacks the branch's commits; every
35
+ # other guard holds, and the sentence names the tip it was at
36
+ def initialize(git, checkout, branch:, onto:, mode: :land)
31
37
  @git = git
32
38
  @checkout = checkout
33
39
  @from = checkout
34
40
  @branch = ref(branch)
35
41
  @onto = ref(onto)
36
- @retiring = retiring
42
+ @retiring = mode != :land
43
+ @discarding = mode == :discard
37
44
  end
38
45
 
39
46
  # @return [String] a sentence saying what was removed, or why nothing was
@@ -49,7 +56,21 @@ module Hecks
49
56
  tree = worktree_on(@branch)
50
57
  tip = tree && branch_tip(@branch)
51
58
  why = reason_to_keep(tree, tip)
52
- why ? [true, why] : drop(tree.path, tip)
59
+ return [true, why] if why
60
+
61
+ note = discarded_note(tip)
62
+ kept, said = drop(tree.path, tip)
63
+ [kept, kept || !@discarding ? said : "#{said}; #{note}"]
64
+ end
65
+
66
+ # What `retire` would do, said and not done: it removes nothing and takes no lock.
67
+ #
68
+ # @return [String] "would retire it", or "would keep it: " and why
69
+ def verdict
70
+ @checkout = primary.path
71
+ tree = worktree_on(@branch)
72
+ why = reason_to_keep(tree, tree && branch_tip(@branch))
73
+ why ? "would keep it: #{why}" : "would retire it"
53
74
  end
54
75
 
55
76
  private
@@ -89,14 +110,6 @@ module Hecks
89
110
  lines.size > NAMED ? "#{shown} and #{lines.size - NAMED} more" : shown
90
111
  end
91
112
 
92
- def unmerged(tip)
93
- return "it has no commit to verify" unless tip
94
-
95
- local = "refs/heads/#{@onto}"
96
- behind = count("#{local}..#{tip}")
97
- "it has #{behind} commit(s) not on #{@onto}" if behind.positive? || !ancestor?(tip, local)
98
- end
99
-
100
113
  def drop(path, tip)
101
114
  return [true, "#{@branch} moved after it was checked"] unless branch_tip(@branch) == tip
102
115
 
@@ -7,6 +7,7 @@ require_relative "git_landing/tidy"
7
7
  require_relative "git_landing/rivals"
8
8
  require_relative "git_landing/finisher"
9
9
  require_relative "git_landing/lineup"
10
+ require_relative "git_landing/flagged"
10
11
  require_relative "../../indifferent_key"
11
12
 
12
13
  module Hecks
@@ -19,6 +20,7 @@ module Hecks
19
20
  class GitLanding
20
21
  include GitWorktrees
21
22
  include GitMainPull
23
+ include Flagged
22
24
 
23
25
  # Accepts the arguments every driven adapter is built with; only the registry and the
24
26
  # overrides are kept.
@@ -96,16 +98,20 @@ module Hecks
96
98
  # @raise [ConsoleCapture::Failure] when anything makes keeping it the safe answer: the primary
97
99
  # worktree, one the process stands in, any file not tracked and clean (ignored included), a
98
100
  # branch with commits `onto` lacks or a protected branch, or git refusing the removal
99
- def remove(**held)
100
- settle(held)
101
- branch = plain(held[:branch]) || current_branch
102
- onto = plain(held[:onto]) || "main"
103
- gone = @checkout
104
- kept, said = Tidy.new(@git, @checkout, branch: branch, onto: onto, retiring: true).retire
105
- raise ConsoleCapture::Failure, "#{gone} is kept: #{said}" if kept
101
+ def remove(**held) = take_down(held, :retire, "retired")
106
102
 
107
- { outcome: { value: "retired" }, checkout: { value: gone }, branch: { value: branch },
108
- report: { value: said } }
103
+ # Removes a linked worktree and deletes its branch although `onto` lacks the branch's commits.
104
+ #
105
+ # Every other guard of `remove` holds: the primary worktree, one the process stands in, any
106
+ # file not tracked and clean, a protected branch and a branch that moved are all refused. The
107
+ # answer names the tip the branch was at and how many commits `onto` lacked, so they can be
108
+ # found again, and the reason the caller gave.
109
+ #
110
+ # @param held [Hash] the `Landing` record: `checkout`, `branch`, `onto` and `reason`
111
+ # @return [Hash{Symbol => Hash}] `outcome:` (`discarded`), `checkout:`, `branch:`, `report:`
112
+ # @raise [ConsoleCapture::Failure] when anything makes keeping it the safe answer
113
+ def drop_branch(**held)
114
+ take_down(held, :discard, "discarded") { |said| "#{said}; reason: #{plain(held[:reason])}" }
109
115
  end
110
116
 
111
117
  # The branches whose commits are not on a branch, or that hold uncommitted work.
@@ -124,6 +130,17 @@ module Hecks
124
130
 
125
131
  private
126
132
 
133
+ def take_down(held, mode, outcome)
134
+ settle(held)
135
+ branch = plain(held[:branch]) || current_branch
136
+ gone = @checkout
137
+ kept, said = Tidy.new(@git, @checkout, branch: branch, onto: plain(held[:onto]) || "main", mode: mode).retire
138
+ raise ConsoleCapture::Failure, "#{gone} is kept: #{said}" if kept
139
+
140
+ { outcome: { value: outcome }, checkout: { value: gone }, branch: { value: branch },
141
+ report: { value: block_given? ? yield(said) : said } }
142
+ end
143
+
127
144
  # One landing from another worktree, as the train makes it for a waiting branch.
128
145
  def land_for(checkout, branch, onto)
129
146
  mine = @checkout
@@ -135,9 +152,8 @@ module Hecks
135
152
 
136
153
  def land_alone(branch, onto, note)
137
154
  refuse_unsaved(branch)
138
- refuse_stale_base(branch) if onto == "main"
139
155
  refuse_rival(branch, onto)
140
- said = tidy(Merger.new(@git, @checkout, branch: branch, onto: onto).land, branch, onto)
156
+ said = on_main(branch, onto) { tidy(Merger.new(@git, @checkout, branch: branch, onto: onto).land, branch, onto) }
141
157
  note ? said.merge(report: { value: "#{said[:report][:value]}; #{note}" }) : said
142
158
  end
143
159
 
@@ -159,11 +175,12 @@ module Hecks
159
175
  landed.merge(report: { value: "#{landed[:report][:value]}; #{said}" })
160
176
  end
161
177
 
162
- # A finish runs inside the worktree it retires, so the process leaves it first; a worktree that
178
+ # A finish runs inside the worktree it retires, so the process leaves it first; a worktree
179
+ # that
163
180
  # holds ignored files is kept and said so.
164
181
  def retire_from_primary(branch, onto)
165
182
  Dir.chdir(primary.path) do
166
- kept, said = Tidy.new(@git, @checkout, branch: branch, onto: onto, retiring: true).retire
183
+ kept, said = Tidy.new(@git, @checkout, branch: branch, onto: onto, mode: :retire).retire
167
184
  kept ? "kept #{branch}: #{said}" : said
168
185
  end
169
186
  end
@@ -0,0 +1,7 @@
1
+ # Required by name, not relative to this file: a fuzz or model check boots a copy of this directory,
2
+ # and the implementation must load once, from `lib/`, not from the copy.
3
+ require "hecks/hecks/adapters/git_leftovers"
4
+
5
+ Hecks.adapter "GitLeftovers" do
6
+ port "Leftovers"
7
+ end