Index: src/services/guardianship/gated.js
===================================================================
--- src/services/guardianship/gated.js	(revision 6d5ce0cde5b3d2911f42fcf2b4cd343d687c8c3b)
+++ src/services/guardianship/gated.js	(revision 9ed940e294372071d487529d16df7a871ecd7bcf)
@@ -182,8 +182,8 @@
 export function clearGatedReviews(id) { rstmts().delAll.run(id); }
 
-export function rememberGatedOffer(offerId, slug, feature, value) {
+export function rememberGatedOffer(offerId, slug, feature, value, proposer) {
   try {
-    db.prepare('INSERT OR REPLACE INTO ap_gated_offers (offer_id, slug, feature, value) VALUES (?,?,?,?)')
-      .run(offerId, slug, feature, value ? 1 : 0);
+    db.prepare('INSERT OR REPLACE INTO ap_gated_offers (offer_id, slug, feature, value, proposer) VALUES (?,?,?,?,?)')
+      .run(offerId, slug, feature, value ? 1 : 0, proposer || null);
   } catch { /* non-fatal */ }
 }
@@ -192,4 +192,48 @@
   try { return db.prepare('SELECT * FROM ap_gated_offers WHERE offer_id = ?').get(offerId) || null; }
   catch { return null; }
+}
+
+// ── The proposer's own record (5.6) ───────────────────────────────
+// "Where did my proposal go?" had no answer: the status was a button caption
+// that did not survive a refresh. The ward's server tallies elsewhere, so the
+// proposer keeps its own row and the ward's server ANSWERS the Offer when the
+// decision settles: Accept when it settled on the proposed value, Reject when
+// it settled on the opposite. An open row past the window renders as expired,
+// because an expired decision settles on nothing and nobody writes home.
+
+export function recordSent(offerId, guardianSlug, wardUri, feature, value) {
+  try {
+    db.prepare(`INSERT OR REPLACE INTO ap_gated_sent (offer_id, guardian_slug, ward_uri, feature, value)
+                VALUES (?,?,?,?,?)`).run(offerId, guardianSlug, wardUri, feature, value ? 1 : 0);
+  } catch { /* non-fatal */ }
+}
+
+export function recallSent(offerId) {
+  try { return db.prepare('SELECT * FROM ap_gated_sent WHERE offer_id = ?').get(offerId) || null; }
+  catch { return null; }
+}
+
+export function settleSent(offerId, outcome) {
+  try { db.prepare('UPDATE ap_gated_sent SET status = ? WHERE offer_id = ?').run(outcome, offerId); } catch { /* non-fatal */ }
+}
+
+/** The latest proposal per feature this guardian sent to this ward. */
+export function listSent(guardianSlug, wardUri) {
+  try {
+    return db.prepare(`SELECT * FROM ap_gated_sent WHERE guardian_slug = ? AND ward_uri = ?
+                       GROUP BY feature HAVING MAX(created_at) ORDER BY created_at DESC`).all(guardianSlug, wardUri);
+  } catch { return []; }
+}
+
+/**
+ * What a sent row means on a screen. Pure, so the rule is testable: an answer
+ * wins, and silence past the window is not "still running", it is over.
+ */
+export function sentStatus(row, now) {
+  if (!row) return null;
+  if (row.status === 'accepted' || row.status === 'rejected') return row.status;
+  const opened = new Date(String(row.created_at).includes('T') ? row.created_at : `${row.created_at}Z`.replace(' ', 'T')).getTime();
+  if (Number.isFinite(opened) && now - opened >= GATED_WINDOW_MS) return 'expired';
+  return 'open';
 }
 
@@ -198,3 +242,4 @@
   parseGatedSetting, buildGatedOffer, rememberGatedOffer, recallGatedOffer,
   recordGatedReview, getGatedReview, listGatedReviews, removeGatedReview, clearGatedReviews,
+  recordSent, recallSent, settleSent, listSent, sentStatus,
 };
Index: src/services/guardianship/handshake.js
===================================================================
--- src/services/guardianship/handshake.js	(revision 6d5ce0cde5b3d2911f42fcf2b4cd343d687c8c3b)
+++ src/services/guardianship/handshake.js	(revision 9ed940e294372071d487529d16df7a871ecd7bcf)
@@ -81,4 +81,24 @@
   }
   return anyDelivered;
+}
+
+/**
+ * §5.6, the closing of the loop: a settled gated decision answers the Offer
+ * that opened it. Accept when it settled on the proposed value, Reject when on
+ * the opposite. Without this the proposer's screen can only ever say
+ * "waiting", forever, whatever actually happened: the tally lives on the
+ * ward's server and nobody else may read it, so the ward's server must speak.
+ */
+function answerGatedProposer(site, offerId, r) {
+  const o = gated.recallGatedOffer(offerId);
+  if (!o || !o.proposer) return;
+  const me = deps.selfId(site.slug);
+  if (o.proposer === me) return;   // the ward proposed to itself: nothing to write home
+  const agreed = r.value === !!o.value;
+  deps.deliverTo(site, o.proposer, {
+    id: `${me}#gatedanswer-${Date.now().toString(36)}${Math.floor(Math.random() * 1e4).toString(36)}`,
+    type: agreed ? 'Accept' : 'Reject',
+    actor: me, to: [o.proposer], object: offerId,
+  }).catch(() => { /* the delivery queue retries */ });
 }
 
@@ -301,5 +321,5 @@
       // ── I am the WARD: record, tally, and forward to the other guardians.
       if (gs.ward === me) {
-        gated.rememberGatedOffer(offerId, site.slug, gs.feature, gs.value);
+        gated.rememberGatedOffer(offerId, site.slug, gs.feature, gs.value, actor);
         // The proposer's Offer carries its own agreement (§3.1's one-step clause).
         const r = gated.recordGatedVote(site.slug, gs.feature, actor, gs.value);
@@ -326,4 +346,5 @@
         } else {
           gated.clearGatedReviews(offerId);   // settled at once: nothing left to ask
+          answerGatedProposer(site, offerId, r);
         }
         notify(site.slug, { kind: 'gated_setting', feature: gs.feature, value: gs.value, state: r.state });
@@ -385,4 +406,16 @@
   // Accept / Reject of an offer we (also) track.
   const offerId = idOf(activity.object);
+  // §5.6, the answer coming HOME: the ward's server settled a decision we
+  // proposed and answers our Offer. Accept = it settled on what we proposed,
+  // Reject = on the opposite. Only the ward may say so: the answer must come
+  // from the ward the proposal was about, or anyone could close our books.
+  const sent = gated.recallSent(offerId);
+  if (sent && sent.guardian_slug === site.slug) {
+    if (actor !== sent.ward_uri) return false;   // not the ward's voice: not an outcome
+    const outcome = type === 'Accept' ? 'accepted' : 'rejected';
+    gated.settleSent(offerId, outcome);
+    notify(site.slug, { kind: 'gated_outcome', feature: sent.feature, value: !!sent.value, outcome, ward: sent.ward_uri });
+    return true;
+  }
   // §5.6: a fellow guardian answering a gated-setting proposal. The Accept only
   // references the offer, so the value comes from the proposal we stored. A
@@ -392,4 +425,5 @@
     const value = type === 'Accept' ? !!gsOffer.value : !gsOffer.value;
     const r = gated.recordGatedVote(site.slug, gsOffer.feature, actor, value);
+    if (r.state === 'settled') answerGatedProposer(site, offerId, r);
     notify(site.slug, { kind: 'gated_setting', feature: gsOffer.feature, value, state: r.state });
     return true;
