From e5cc2ef0ead95cd51f97c75dc5e1e2e2ca196646 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 03:58:53 +0000 Subject: [PATCH] feat(merge-orchestration): runtime increment 4 -- Dispatcher wiring (decision -> dispatch pipeline + lease) The last runtime increment -- connects the decision modules to hypatia's existing pipeline: - lib/merge_orchestration/dispatcher.ex: maps a Strategist decision (route x method x safety) onto Hypatia.DispatchManifest's entry shape (the tier/strategy vocabulary the dispatch-runner + the .git-private-farm actuator already consume), and mints the Kin.Gate coordination lease (a5 schema) for the PR territory. Pure; the I/O is the caller's thin glue, so it stays test-isolated. - test: 5 ExUnit cases. Full merge_orchestration suite now 20 tests, 0 failures (local elixir 1.14). Runtime structurally complete: Strategist + Kin.Council + Dispatcher (hypatia) + the actuator (.git-private-farm #77). The spec -> running-code path is end-to-end. https://claude.ai/code/session_011GXPoh6pB6rm3jfeLHWMtc --- .../merge-orchestration/LEDGER.a2ml | 2 +- lib/merge_orchestration/dispatcher.ex | 81 +++++++++++++++++++ test/merge_orchestration/dispatcher_test.exs | 56 +++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 lib/merge_orchestration/dispatcher.ex create mode 100644 test/merge_orchestration/dispatcher_test.exs diff --git a/.machine_readable/merge-orchestration/LEDGER.a2ml b/.machine_readable/merge-orchestration/LEDGER.a2ml index ed3585b..feb9479 100644 --- a/.machine_readable/merge-orchestration/LEDGER.a2ml +++ b/.machine_readable/merge-orchestration/LEDGER.a2ml @@ -66,4 +66,4 @@ hold = "6a2/STATE,META + anchors/ANCHOR in {hypatia,gitbot-fleet,.git-p other-agents = "claude/nice-ptolemy-lNB8E (standards), claude/loving-cannon-AtUSm (echidna), claude/abi-tier2-and-l10-trans-cycle (vcl-ut), claude/affinescript-migration-wip (reposystem)" [next] -step = "Wave-0 design COMPLETE (a1-a5). RUNTIME: increment 1 = Strategist (decision head); increment 2 = Kin.Council (competence-weighted veto aggregation over a3 attestations, NOT Byzantine; wired into Strategist, replacing the mean stub). 15 ExUnit tests total (6 council + 9 strategist), 0 failures, local elixir 1.14. NEXT INCREMENTS: increment 3 = .git-private-farm actuator (read decision -> re-verify invariants -> execute merge method); increment 4 = wire Strategist/Council into dispatch_manifest + triangle_router + Kin.Gate. Owner follow-ons: required-checks per repo; a5 standards adoption." +step = "Wave-0 design COMPLETE (a1-a5). RUNTIME COMPLETE (4 increments): 1 Strategist (decision head) + 2 Kin.Council (competence-weighted veto) + 4 Dispatcher (wiring: decision -> DispatchManifest tier/strategy vocabulary + Kin.Gate lease) [hypatia, 20 ExUnit, 0 failures]; 3 actuator (independent re-verify + execute, 5 bash checks) [.git-private-farm, merged #77]. spec -> running-code path complete end-to-end, all locally tested on elixir 1.14. REMAINING (operationalisation, not design): connect Dispatcher's thin glue into pattern_analyzer/fleet_dispatcher (the DispatchManifest.write call) + real Kin.Gate lease storage + GoT/MoE weight wiring into KinCouncil; owner follow-ons (required-checks per repo; a5 standards adoption; P3 handshake + mass_squash in actuator; the .git-private-farm Actions billing/CI is dead -- fix before the actuator can run scheduled)." diff --git a/lib/merge_orchestration/dispatcher.ex b/lib/merge_orchestration/dispatcher.ex new file mode 100644 index 0000000..40f022f --- /dev/null +++ b/lib/merge_orchestration/dispatcher.ex @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell +defmodule Hypatia.MergeOrchestration.Dispatcher do + @moduledoc """ + Wiring — the merge-orchestration runtime's nervous system. + + It connects a `Strategist` decision (route × method × safety) to hypatia's + *existing* dispatch pipeline by mapping it onto `Hypatia.DispatchManifest`'s + entry shape — the `tier` / `strategy` vocabulary (`eliminate|substitute|control` + × `auto_execute|review|report_only`) that the dispatch-runner and the + `.git-private-farm` actuator already consume — and it mints the Kin.Gate + coordination lease (artifact 5) for the PR's territory. + + Pure: it returns the manifest entry + lease as data. The I/O (writing the + manifest via `DispatchManifest.write/1`, acquiring the lease in `Kin.Gate`) is + the caller's thin glue, so this stays test-isolated. + """ + + alias Hypatia.MergeOrchestration.Strategist + + @doc "Decide, then return `{decision, manifest_entry, lease}`." + def dispatch(ctx) do + decision = Strategist.decide(ctx) + {decision, to_manifest_entry(decision), lease_for(ctx, decision)} + end + + @doc "Map a decision onto the existing DispatchManifest entry shape, plus a `merge` extension." + def to_manifest_entry(decision) do + %{ + "tier" => tier(decision.safety), + "strategy" => strategy(decision.safety), + "repo" => decision.pr.repo, + "confidence" => decision.confidence, + "severity" => "Info", + "timestamp" => now(), + "merge" => %{ + "pr_number" => decision.pr.number, + "method" => to_string(decision.method), + "safety" => to_string(decision.safety), + "route" => decision.route, + "vetoes" => decision.vetoes, + "change_class" => to_string(decision.change_class), + "change_level" => to_string(decision.change_level), + "pool" => to_string(decision.pool), + "rationale" => decision.rationale + } + } + end + + @doc "Mint the Kin.Gate coordination lease (artifact-5 schema) for this decision's territory." + def lease_for(ctx, decision) do + %{ + "lease_id" => "lease-" <> String.replace(decision.pr.repo, "/", "-") <> "-" <> to_string(decision.pr.number), + "holder" => Map.get(ctx, :holder, "hypatia"), + "repo" => decision.pr.repo, + "territory" => %{ + "branch" => Map.get(ctx, :branch, "main"), + "paths" => Map.get(ctx, :paths, []), + "is_meta" => decision.change_level == :meta + }, + "intent" => "merge-orchestration decision", + "status" => "held", + "acquired_at" => now(), + "expires_at" => later(3600), + # The dispatcher never self-authorises a meta claim (LE2); owner must. + "owner_authorized" => Map.get(ctx, :owner_authorized, false) + } + end + + # safety axis -> the existing safety-triangle dispatch vocabulary + defp strategy(:arm_auto), do: "auto_execute" + defp strategy(:review), do: "review" + defp strategy(:flag), do: "report_only" + + defp tier(:arm_auto), do: "eliminate" + defp tier(:review), do: "substitute" + defp tier(:flag), do: "control" + + defp now, do: DateTime.utc_now() |> DateTime.to_iso8601() + defp later(secs), do: DateTime.utc_now() |> DateTime.add(secs, :second) |> DateTime.to_iso8601() +end diff --git a/test/merge_orchestration/dispatcher_test.exs b/test/merge_orchestration/dispatcher_test.exs new file mode 100644 index 0000000..0eb60f7 --- /dev/null +++ b/test/merge_orchestration/dispatcher_test.exs @@ -0,0 +1,56 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell +defmodule Hypatia.MergeOrchestration.DispatcherTest do + use ExUnit.Case, async: true + alias Hypatia.MergeOrchestration.Dispatcher + + defp ctx(overrides) do + Map.merge( + %{ + pr: %{repo: "hyperpolymath/panll", number: 412}, + change_class: :chore, + change_level: :object, + pool: :p2, + attestations: [%{bot: "ci", verdict: :approve, confidence: 0.99}] + }, + overrides + ) + end + + test "arm_auto maps onto the existing dispatch vocabulary: auto_execute / eliminate" do + {_d, m, _l} = Dispatcher.dispatch(ctx(%{})) + assert m["strategy"] == "auto_execute" + assert m["tier"] == "eliminate" + assert m["merge"]["method"] == "squash" + assert m["repo"] == "hyperpolymath/panll" + end + + test "a veto -> report_only / control, and the veto rides along in the manifest" do + {_d, m, _l} = Dispatcher.dispatch(ctx(%{attestations: [%{bot: "panicbot", verdict: :veto, rationale: "license/SPDX"}]})) + assert m["strategy"] == "report_only" + assert m["tier"] == "control" + assert [%{bot: "panicbot"}] = m["merge"]["vetoes"] + end + + test "P1 clamp -> review / substitute" do + {_d, m, _l} = Dispatcher.dispatch(ctx(%{pool: :p1})) + assert m["strategy"] == "review" + assert m["tier"] == "substitute" + end + + test "a lease is minted for the PR territory, with a TTL (LE1)" do + {_d, _m, l} = Dispatcher.dispatch(ctx(%{branch: "dependabot/cargo/x", paths: ["Cargo.lock"]})) + assert l["status"] == "held" + assert l["repo"] == "hyperpolymath/panll" + assert l["territory"]["branch"] == "dependabot/cargo/x" + assert l["territory"]["is_meta"] == false + assert l["expires_at"] > l["acquired_at"] + end + + test "a meta change -> control, and the lease marks is_meta and stays owner-unauthorised (LE2)" do + {_d, m, l} = Dispatcher.dispatch(ctx(%{change_level: :meta})) + assert m["tier"] == "control" + assert l["territory"]["is_meta"] == true + assert l["owner_authorized"] == false + end +end