diff --git a/.machine_readable/merge-orchestration/LEDGER.a2ml b/.machine_readable/merge-orchestration/LEDGER.a2ml
index e534cc0d..7a558c13 100644
--- a/.machine_readable/merge-orchestration/LEDGER.a2ml
+++ b/.machine_readable/merge-orchestration/LEDGER.a2ml
@@ -44,7 +44,8 @@ later  = "verisimdb entity model + PolicyDrift; CodeCite/ProofTrace"
 
 [wave-0-artifacts]
 a1 = "decision contract — LANDED on hypatia/main (PR #442, merged 2026-06-05). doc + schema + +/- fixtures, ajv-validated."
-a2 = "CVE/bump gate — this commit. 01-cve-bump-gate.adoc + schemas/bump-decision.schema.json (extends a1) + bump fixtures, ajv-validated."
+a2 = "CVE/bump gate — LANDED on hypatia/main (PR #443, merged 2026-06-05). bump-decision.schema (extends a1) + bump fixtures, ajv-validated."
+a3 = "attestation — this commit. 02-attestation.adoc + schemas/attestation.schema.json + structural fixtures; ed25519 sign/verify/tamper round-trip PASSED + ajv-validated (signed-valid verifies; verdict-tamper -> integrity fail; sig-tamper -> authenticity fail)."
 
 [gated-pr-ledger]
 # Applying the gate by hand to all open PRs across 11 in-scope repos (2026-06-05).
@@ -63,4 +64,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 = "finish CVE/bump-gate artifact (this commit); then run live `panic-attack bridge triage` on the held bumps (needs panic-attack built --features http + OSV network) to emit gated decisions for the backlog."
+step = "attestation landed (this commit); NEXT = pool<->TRUST + Kin-backed lease/coordination spec (in standards). Open operational follow-on: live `panic-attack bridge triage` on the held bumps."
diff --git a/docs/design/merge-orchestration/02-attestation.adoc b/docs/design/merge-orchestration/02-attestation.adoc
new file mode 100644
index 00000000..74937e0a
--- /dev/null
+++ b/docs/design/merge-orchestration/02-attestation.adoc
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: MPL-2.0
+// Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
+= Merge Orchestration — Bot Attestations (Wave-0, artifact 3)
+:toc: macro
+
+[IMPORTANT]
+====
+A bot's opinion enters the Kin Council as a *signed* attestation, not a bare
+claim. This reuses panic-attack's `intent → evidence → seal` chain (Ed25519).
+Spec + schema + structural fixtures + a *real* sign / verify / tamper round-trip.
+The permanent verifier is Rust/Elixir at runtime; this artifact proves the scheme.
+====
+
+== Shape
+
+[source]
+----
+attestation: {
+  bot, subject{repo, number, head_sha},
+  verdict (approve|hold|veto), confidence, rationale,
+  intent_hash, evidence_hash,
+  seal{ chain_hash, alg="ed25519", sig, pubkey },
+  nonce, tool_version, timestamp
+}
+----
+
+== The chain (reused from panic-attack `attestation/`)
+
+[source]
+----
+intent_hash   = sha256(tool_version | subject | nonce | timestamp)   # pre-commitment
+evidence_hash = sha256(what the bot examined)                        # rolling evidence
+chain_hash    = sha256(intent_hash | evidence_hash | verdict)        # binding
+seal.sig      = Ed25519(chain_hash)                                  # authenticity
+----
+
+Verification has *two* halves: *integrity* (recompute `chain_hash` from the
+fields and compare to `seal.chain_hash`) AND *authenticity* (Ed25519-verify
+`seal.sig` over `seal.chain_hash` with `seal.pubkey`). Tampering any covered
+field breaks integrity; forging without the key breaks authenticity.
+
+== Three structural invariants (schema-encoded)
+
+[cols="1,4",options=header]
+|===
+| Invariant | Rule
+| *A1* | Unsigned ⇒ reject. `seal{chain_hash, alg, sig}` are all required.
+| *A2* | `seal.alg` must be `ed25519` — no `none` / weak algorithms.
+| *A3* | A `veto` must carry a non-empty `rationale`.
+|===
+
+== How it feeds the council (artifact 1)
+
+The decision manifest's `attestations[]` entries *are* these (the heavy `seal`
+is stored / looked up by `chain_hash`). The `authority_bot`'s attestation is
+weighted in the Kin Council; any `verdict = veto` becomes a manifest veto, which
+the monotone-veto invariant forces to `safety = flag`.
+
+== Tested for real
+
+* *Schema (ajv 8):* `signed-valid.json` passes; `negative/neg-unsigned.json`
+  (A1), `neg-alg-none.json` (A2), `neg-veto-no-rationale.json` (A3) all rejected.
+* *Ed25519 round-trip (`node:crypto`):* a genuine attestation *verifies*;
+  tampering the `verdict` is *rejected* (integrity); corrupting the `sig` is
+  *rejected* (authenticity). `signed-valid.json` carries a real signature +
+  public key and is re-verifiable.
+
+Permanent home: a Rust/Elixir verifier test when the runtime lands.
diff --git a/docs/design/merge-orchestration/fixtures/attestation/negative/neg-alg-none.json b/docs/design/merge-orchestration/fixtures/attestation/negative/neg-alg-none.json
new file mode 100644
index 00000000..5aa82e7e
--- /dev/null
+++ b/docs/design/merge-orchestration/fixtures/attestation/negative/neg-alg-none.json
@@ -0,0 +1,14 @@
+{
+  "$comment": "SPDX-License-Identifier: MPL-2.0 — INTENTIONALLY INVALID. seal.alg=none (not ed25519). MUST be REJECTED (A2). Not a real attestation.",
+  "bot": "panicbot",
+  "subject": {"repo": "hyperpolymath/example", "number": 1, "head_sha": "abc123"},
+  "verdict": "approve",
+  "confidence": 0.9,
+  "rationale": "looks fine",
+  "intent_hash": "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+  "evidence_hash": "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+  "seal": {"chain_hash": "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "alg": "none", "sig": "0000"},
+  "nonce": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+  "tool_version": "panic-attack/2.5.0",
+  "timestamp": "2026-06-05T15:00:00Z"
+}
diff --git a/docs/design/merge-orchestration/fixtures/attestation/negative/neg-unsigned.json b/docs/design/merge-orchestration/fixtures/attestation/negative/neg-unsigned.json
new file mode 100644
index 00000000..27ee2486
--- /dev/null
+++ b/docs/design/merge-orchestration/fixtures/attestation/negative/neg-unsigned.json
@@ -0,0 +1,13 @@
+{
+  "$comment": "SPDX-License-Identifier: MPL-2.0 — INTENTIONALLY INVALID. No seal (unsigned). MUST be REJECTED (A1). Not a real attestation.",
+  "bot": "panicbot",
+  "subject": {"repo": "hyperpolymath/example", "number": 1, "head_sha": "abc123"},
+  "verdict": "approve",
+  "confidence": 0.9,
+  "rationale": "looks fine",
+  "intent_hash": "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+  "evidence_hash": "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+  "nonce": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+  "tool_version": "panic-attack/2.5.0",
+  "timestamp": "2026-06-05T15:00:00Z"
+}
diff --git a/docs/design/merge-orchestration/fixtures/attestation/negative/neg-veto-no-rationale.json b/docs/design/merge-orchestration/fixtures/attestation/negative/neg-veto-no-rationale.json
new file mode 100644
index 00000000..c6e0e993
--- /dev/null
+++ b/docs/design/merge-orchestration/fixtures/attestation/negative/neg-veto-no-rationale.json
@@ -0,0 +1,13 @@
+{
+  "$comment": "SPDX-License-Identifier: MPL-2.0 — INTENTIONALLY INVALID. verdict=veto with no rationale. MUST be REJECTED (A3). Not a real attestation.",
+  "bot": "echidnabot",
+  "subject": {"repo": "hyperpolymath/example", "number": 2, "head_sha": "def456"},
+  "verdict": "veto",
+  "confidence": 1.0,
+  "intent_hash": "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+  "evidence_hash": "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+  "seal": {"chain_hash": "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "alg": "ed25519", "sig": "abcd"},
+  "nonce": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+  "tool_version": "echidnabot/1.0.0",
+  "timestamp": "2026-06-05T15:00:00Z"
+}
diff --git a/docs/design/merge-orchestration/fixtures/attestation/signed-valid.json b/docs/design/merge-orchestration/fixtures/attestation/signed-valid.json
new file mode 100644
index 00000000..d2790ee4
--- /dev/null
+++ b/docs/design/merge-orchestration/fixtures/attestation/signed-valid.json
@@ -0,0 +1,23 @@
+{
+  "$comment": "SPDX-License-Identifier: MPL-2.0 — GENERATED signed attestation (real Ed25519). Re-verifiable via the embedded seal.pubkey. Not hand-written.",
+  "bot": "ci",
+  "subject": {
+    "repo": "hyperpolymath/hypatia",
+    "number": 442,
+    "head_sha": "3aa68ac"
+  },
+  "verdict": "approve",
+  "confidence": 0.99,
+  "rationale": "decision contract: docs-only, object-level, CI green",
+  "intent_hash": "sha256:e18ee59c5ce5c39a3d97d7db023b14e67f1174b48ce9eaba02229986f063a692",
+  "evidence_hash": "sha256:0db97e31ad7c4a30653b46c8cdc4652a2e3f94c54a35e092e8f7135145de80cf",
+  "seal": {
+    "chain_hash": "sha256:06cc4bb255ce7d472de0f16ab81046dbbc9bd476a6f15034a1a7cc9d205f97db",
+    "alg": "ed25519",
+    "sig": "8b70b8960f2eb2bbcfa76794314cc002ff4ea3d48fe4f923acd451d46ec07c030222d5fba8b53fd29de0f55a12788ba0bdc20697958530d3790fff965b5d5d07",
+    "pubkey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAmGLva1NmjvXq16noyjV8pUd4zsmvRMj8+OJaERokOxk=\n-----END PUBLIC KEY-----\n"
+  },
+  "nonce": "baaafec4988d78c5e9751cadd0425100375934d8a2f0b7b40ec83b43143c0630",
+  "tool_version": "panic-attack/2.5.0",
+  "timestamp": "2026-06-05T15:00:00Z"
+}
diff --git a/docs/design/merge-orchestration/schemas/attestation.schema.json b/docs/design/merge-orchestration/schemas/attestation.schema.json
new file mode 100644
index 00000000..f045bca8
--- /dev/null
+++ b/docs/design/merge-orchestration/schemas/attestation.schema.json
@@ -0,0 +1,43 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$id": "https://github.com/hyperpolymath/hypatia/blob/main/docs/design/merge-orchestration/schemas/attestation.schema.json",
+  "$comment": "SPDX-License-Identifier: MPL-2.0 — signed bot attestation (Wave-0 artifact 3). Reuses panic-attack intent->evidence->seal. Copyright (c) 2026 Jonathan D.A. Jewell.",
+  "title": "BotAttestation",
+  "type": "object",
+  "additionalProperties": true,
+  "required": ["bot", "subject", "verdict", "confidence", "intent_hash", "evidence_hash", "seal", "nonce", "tool_version", "timestamp"],
+  "properties": {
+    "bot": {"type": "string"},
+    "subject": {
+      "type": "object",
+      "required": ["repo", "number", "head_sha"],
+      "properties": {"repo": {"type": "string"}, "number": {"type": "integer", "minimum": 1}, "head_sha": {"type": "string"}}
+    },
+    "verdict": {"enum": ["approve", "hold", "veto"]},
+    "confidence": {"type": "number", "minimum": 0, "maximum": 1},
+    "rationale": {"type": "string"},
+    "intent_hash": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"},
+    "evidence_hash": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"},
+    "seal": {
+      "type": "object",
+      "$comment": "A1 — unsigned attestations are rejected: chain_hash + alg + sig are all required.",
+      "required": ["chain_hash", "alg", "sig"],
+      "properties": {
+        "chain_hash": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"},
+        "alg": {"const": "ed25519", "$comment": "A2 — no 'none'/weak algorithms."},
+        "sig": {"type": "string", "minLength": 1},
+        "pubkey": {"type": "string"}
+      }
+    },
+    "nonce": {"type": "string", "minLength": 32},
+    "tool_version": {"type": "string"},
+    "timestamp": {"type": "string"}
+  },
+  "allOf": [
+    {
+      "$comment": "A3 — a veto must carry a non-empty rationale.",
+      "if": {"properties": {"verdict": {"const": "veto"}}},
+      "then": {"required": ["rationale"], "properties": {"rationale": {"type": "string", "minLength": 1}}}
+    }
+  ]
+}
