Skip to content

fix: keep Discord decision questions readable after settlement - #256

Merged
dnth merged 8 commits into
mainfrom
fm/fm-ext-decision-keep-question
Oct 16, 2026
Merged

dnth merged 8 commits into
mainfrom
fm/fm-ext-decision-keep-question

Conversation

@dnth

@dnth dnth commented Oct 16, 2026

Copy link
Copy Markdown
Owner

Intent

When a Discord decision post is settled, keep its question readable instead of replacing the message text with a single outcome line. After a decision button is clicked, the message is edited and the question disappears; the captain wants to return later and still see both the question and the decision. The captain approved proposal items 1–4; the decision log (item 5) is out of scope. Every outcome state—chosen, chosen by the captain elsewhere, decided without a unique option, waiting to be recorded, closed because it is no longer needed, and replaced—must edit only the last chunk (the chunk carrying the buttons) to put one bold outcome line above that chunk’s original text unchanged; the example header is “✅ Decided: Fix Pergub 33 first (dnth, )”. Buttons stay visible and disabled: the uniquely chosen option is success-styled with a ✓ in its label, while other options and Discuss are grey; when no option is chosen, all buttons are disabled grey. A replaced post’s header links to the newer post when its message link is known, otherwise it says “Replaced by a newer question”. Respect Discord’s 2,000-character limit by trimming only the tail of the original explanation and adding “(trimmed)”; never trim the header, question, or option list, and leave earlier chunks untouched. Preserve the existing write-once settle and finalize-receipt ledger semantics, retries, and guards (plain-text choice refusal, still-open checks, and occurrence matching) unchanged except for the content shape.

What Changed

  • Preserve decision text beneath bold outcome headers on the final Discord chunk, link replacements when available, and keep disabled buttons visible with the chosen option highlighted.
  • Store question and explanation sections, reserve header space during chunking, and trim only explanation tails when needed to fit Discord’s limit.
  • Preserve legacy message text through button-only updates; document the display rules and extend coverage for settlement states, chunk limits, retries, and historical choices.

Risk Assessment

✅ Low: The changes are bounded and preserve settlement semantics while implementing the accepted question-preservation and legacy button-only behavior.

Testing

Both targeted suites and isolated CLI/poster scenarios passed. Captured actual PATCH payloads against a disposable Discord HTTP substitute. No screenshot was captured because this setup has no Discord renderer; Discord appearance was not exercised. Disposable files were removed.

  • Live validation: ✅ go - 8 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Click an option: waiting and recorded outcomes retain the question and highlight the disabled choice. ✅ pass live Captured product PATCH requests: c2
Record an answer elsewhere, an ambiguous answer, or closure: preserve the question and show the appropriate disabled buttons. ✅ pass live Captured product PATCH requests: c6
Replace a question: show the newer message link when known and fallback wording otherwise. ✅ pass live Captured product PATCH requests: c7 and c17
Settle long posts: edit only the final chunk, stay within 2,000 characters, and trim only explanation text. ✅ pass live live-cli-http.log: case 17; Captured product PATCH requests: c17
Settle every legacy outcome with 1,900-character text and an 80-character option: preserve text bytes and disable the buttons. ✅ pass live live-cli-http.log: case 19; Captured product PATCH requests: c19-*
Recover a legacy waiting receipt without original text or payload: retry a failed edit without advancing its receipt, then settle the buttons. ✅ pass live live-http-retry.log; Captured product PATCH requests: c20, HTTP 429 then HTTP 200
Reopen a settled legacy decision: retain its historical highlighted choice, or all-grey buttons when no choice was recorded. ✅ pass live live-cli-http.log: case 21; Captured product PATCH requests: c21-North, c21-South, c21-unknown
Attempt duplicate, stale, mismatched, and plain-text choices: preserve existing refusal and occurrence guards. ✅ pass live live-cli-http.log: cases 6–7; tests/fm-ext-decision.test.sh; tests/fm-ext-ask.test.sh
Evidence: Captured product PATCH requests
{"home": "c2", "channel": "222222222222222222", "message": "700000000000000001", "content": "**⏳ North sent by Dana <t:1791720000:f> - waiting for Firstmate to record it**\n\nWhich way should the fix go?\n1. North ⭐\n2. South\nReply to this message with the number.", "components": [{"type": 1, "components": [{"type": 2, "style": 3, "label": "North ✓", "custom_id": "fmd:key:route-fix:pick:1", "disabled": true}, {"type": 2, "style": 2, "label": "South", "custom_id": "fmd:key:route-fix:pick:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:pick:d", "disabled": true}]}]}
{"home": "c2", "channel": "222222222222222222", "message": "700000000000000001", "content": "**✅ North chosen by Dana <t:1791720000:f>**\n\nWhich way should the fix go?\n1. North ⭐\n2. South\nReply to this message with the number.", "components": [{"type": 1, "components": [{"type": 2, "style": 3, "label": "North ✓", "custom_id": "fmd:key:route-fix:pick:1", "disabled": true}, {"type": 2, "style": 2, "label": "South", "custom_id": "fmd:key:route-fix:pick:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:pick:d", "disabled": true}]}]}
{"home": "c6", "channel": "222222222222222222", "message": "700000000000000007", "content": "**✅ Decided <t:1791720000:f>**\n\nWhich way should the fix go?\n1. North ⭐\n2. South\nReply to this message with the number.", "components": [{"type": 1, "components": [{"type": 2, "style": 2, "label": "North", "custom_id": "fmd:key:route-fix:amb:1", "disabled": true}, {"type": 2, "style": 2, "label": "South", "custom_id": "fmd:key:route-fix:amb:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:amb:d", "disabled": true}]}]}
{"home": "c6", "channel": "222222222222222222", "message": "700000000000000003", "content": "**✅ South chosen by the captain <t:1791720000:f>**\n\nWhich way should the fix go?\n1. North ⭐\n2. South\nReply to this message with the number.", "components": [{"type": 1, "components": [{"type": 2, "style": 2, "label": "North", "custom_id": "fmd:key:route-fix:num:1", "disabled": true}, {"type": 2, "style": 3, "label": "South ✓", "custom_id": "fmd:key:route-fix:num:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:num:d", "disabled": true}]}]}
{"home": "c6", "channel": "222222222222222222", "message": "700000000000000005", "content": "**Closed - no longer needed <t:1791720000:f>**\n\nWhich way should the fix go?\n1. North ⭐\n2. South\nReply to this message with the number.", "components": [{"type": 1, "components": [{"type": 2, "style": 2, "label": "North", "custom_id": "fmd:key:route-fix:late:1", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:late:d", "disabled": true}]}]}
{"home": "c6", "channel": "222222222222222222", "message": "700000000000000002", "content": "**✅ South chosen by the captain <t:1791720000:f>**\n\nWhich way should the fix go?\n1. North ⭐\n2. South\nReply to this message with the number.", "components": [{"type": 1, "components": [{"type": 2, "style": 2, "label": "North", "custom_id": "fmd:key:route-fix:typed:1", "disabled": true}, {"type": 2, "style": 3, "label": "South ✓", "custom_id": "fmd:key:route-fix:typed:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:typed:d", "disabled": true}]}]}
{"home": "c6", "channel": "222222222222222222", "message": "700000000000000004", "content": "**✅ Decided <t:1791720000:f>**\n\nWhich way should the fix go?\n1. North ⭐\n2. South\nReply to this message with the number.", "components": [{"type": 1, "components": [{"type": 2, "style": 2, "label": "North", "custom_id": "fmd:key:route-fix:chat:1", "disabled": true}, {"type": 2, "style": 2, "label": "South", "custom_id": "fmd:key:route-fix:chat:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:chat:d", "disabled": true}]}]}
{"home": "c6", "channel": "222222222222222222", "message": "700000000000000001", "content": "**✅ North chosen by Dana <t:1791720000:f>**\n\nWhich way should the fix go?\n1. North ⭐\n2. South\nReply to this message with the number.", "components": [{"type": 1, "components": [{"type": 2, "style": 3, "label": "North ✓", "custom_id": "fmd:key:route-fix:pick:1", "disabled": true}, {"type": 2, "style": 2, "label": "South", "custom_id": "fmd:key:route-fix:pick:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:pick:d", "disabled": true}]}]}
{"home": "c6", "channel": "222222222222222222", "message": "700000000000000006", "content": "**✅ South chosen by the captain <t:1791720000:f>**\n\nWhich way should the fix go?\n1. North ⭐\n2. South\nReply to this message with the number.", "components": [{"type": 1, "components": [{"type": 2, "style": 2, "label": "North", "custom_id": "fmd:key:route-fix:change:1", "disabled": true}, {"type": 2, "style": 3, "label": "South ✓", "custom_id": "fmd:key:route-fix:change:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:change:d", "disabled": true}]}]}
{"home": "c20", "channel": "222222222222222222", "message": "700000000000000001", "content": null, "components": [{"type": 1, "components": [{"type": 2, "style": 2, "label": "North", "custom_id": "fmd:key:route-fix:legacy-waiting:1", "disabled": true}, {"type": 2, "style": 3, "label": "South ✓", "custom_id": "fmd:key:route-fix:legacy-waiting:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:legacy-waiting:d", "disabled": true}]}]}
{"home": "c7", "channel": "222222222222222222", "message": "700000000000000010", "content": "♻️ **Replaced by a newer question** ([open](https://discord.com/channels/111111111111111111/222222222222222222/700000000000000011))\n\nWhich way should the fix go?\n1. North ⭐\n2. South\nReply to this message with the number.", "components": [{"type": 1, "components": [{"type": 2, "style": 2, "label": "North", "custom_id": "fmd:key:route-fix:swap:1", "disabled": true}, {"type": 2, "style": 2, "label": "South", "custom_id": "fmd:key:route-fix:swap:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:swap:d", "disabled": true}]}]}
{"home": "c7", "channel": "222222222222222222", "message": "700000000000000011", "content": "**✅ South chosen by Dana <t:1791720000:f>**\n\nWhich way should the fix go?\n1. North ⭐\n2. South\nReply to this message with the number.", "components": [{"type": 1, "components": [{"type": 2, "style": 3, "label": "South ✓", "custom_id": "fmd:key:route-fix:swap:1", "disabled": true}, {"type": 2, "style": 2, "label": "North", "custom_id": "fmd:key:route-fix:swap:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:swap:d", "disabled": true}]}]}
{"home": "c7", "channel": "222222222222222222", "message": "700000000000000001", "content": "♻️ **Replaced by a newer question** ([open](https://discord.com/channels/111111111111111111/222222222222222222/700000000000000002))\n\nWhich way should the fix go?\n1. North ⭐\n2. South\nReply to this message with the number.", "components": [{"type": 1, "components": [{"type": 2, "style": 2, "label": "North", "custom_id": "fmd:key:route-fix:pick:1", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:pick:d", "disabled": true}]}]}
{"home": "c17", "channel": "222222222222222222", "message": "700000000000000102", "content": "**✅ North chosen by the captain <t:1791720000:f>**\n\nQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ

... [2789 bytes truncated] ...

 true}, {"type": 2, "style": 2, "label": "South", "custom_id": "fmd:key:route-fix:explanation-overflow:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:explanation-overflow:d", "disabled": true}]}]}
{"home": "c17", "channel": "222222222222222222", "message": "700000000000000120", "content": null, "components": [{"type": 1, "components": [{"type": 2, "style": 3, "label": "NorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNor ✓", "custom_id": "fmd:key:route-fix:legacy-overflow:1", "disabled": true}, {"type": 2, "style": 2, "label": "South", "custom_id": "fmd:key:route-fix:legacy-overflow:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:legacy-overflow:d", "disabled": true}]}]}
{"home": "c17", "channel": "222222222222222222", "message": "700000000000000130", "content": "**Replaced by a newer question**\n\nWhich way should the fix go?\n1. North ⭐\n2. South\nReply to this message with the number.", "components": [{"type": 1, "components": [{"type": 2, "style": 2, "label": "North", "custom_id": "fmd:key:route-fix:replace-fallback:1", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:replace-fallback:d", "disabled": true}]}]}
{"home": "c19-waiting", "channel": "222222222222222222", "message": "700000000000000001", "content": null, "components": [{"type": 1, "components": [{"type": 2, "style": 3, "label": "NorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNor ✓", "custom_id": "fmd:key:route-fix:legacy:1", "disabled": true}, {"type": 2, "style": 2, "label": "South", "custom_id": "fmd:key:route-fix:legacy:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:legacy:d", "disabled": true}]}], "http_status": 200}
{"home": "c20", "channel": "222222222222222222", "message": "700000000000000001", "content": null, "components": [{"type": 1, "components": [{"type": 2, "style": 2, "label": "North", "custom_id": "fmd:key:route-fix:legacy-waiting:1", "disabled": true}, {"type": 2, "style": 3, "label": "South ✓", "custom_id": "fmd:key:route-fix:legacy-waiting:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:legacy-waiting:d", "disabled": true}]}], "http_status": 429}
{"home": "c20", "channel": "222222222222222222", "message": "700000000000000001", "content": null, "components": [{"type": 1, "components": [{"type": 2, "style": 2, "label": "North", "custom_id": "fmd:key:route-fix:legacy-waiting:1", "disabled": true}, {"type": 2, "style": 3, "label": "South ✓", "custom_id": "fmd:key:route-fix:legacy-waiting:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:legacy-waiting:d", "disabled": true}]}], "http_status": 200}
{"home": "c19-waiting", "channel": "222222222222222222", "message": "700000000000000001", "content": null, "components": [{"type": 1, "components": [{"type": 2, "style": 3, "label": "NorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNor ✓", "custom_id": "fmd:key:route-fix:legacy:1", "disabled": true}, {"type": 2, "style": 2, "label": "South", "custom_id": "fmd:key:route-fix:legacy:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:legacy:d", "disabled": true}]}], "http_status": 200}
{"home": "c19-chosen", "channel": "222222222222222222", "message": "700000000000000001", "content": null, "components": [{"type": 1, "components": [{"type": 2, "style": 3, "label": "NorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNor ✓", "custom_id": "fmd:key:route-fix:legacy:1", "disabled": true}, {"type": 2, "style": 2, "label": "South", "custom_id": "fmd:key:route-fix:legacy:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:legacy:d", "disabled": true}]}], "http_status": 200}
{"home": "c19-captain", "channel": "222222222222222222", "message": "700000000000000001", "content": null, "components": [{"type": 1, "components": [{"type": 2, "style": 3, "label": "NorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNor ✓", "custom_id": "fmd:key:route-fix:legacy:1", "disabled": true}, {"type": 2, "style": 2, "label": "South", "custom_id": "fmd:key:route-fix:legacy:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:legacy:d", "disabled": true}]}], "http_status": 200}
{"home": "c19-ambiguous", "channel": "222222222222222222", "message": "700000000000000001", "content": null, "components": [{"type": 1, "components": [{"type": 2, "style": 2, "label": "NorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorth", "custom_id": "fmd:key:route-fix:legacy:1", "disabled": true}, {"type": 2, "style": 2, "label": "South", "custom_id": "fmd:key:route-fix:legacy:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:legacy:d", "disabled": true}]}], "http_status": 200}
{"home": "c19-closed", "channel": "222222222222222222", "message": "700000000000000001", "content": null, "components": [{"type": 1, "components": [{"type": 2, "style": 2, "label": "NorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorth", "custom_id": "fmd:key:route-fix:legacy:1", "disabled": true}, {"type": 2, "style": 2, "label": "South", "custom_id": "fmd:key:route-fix:legacy:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:legacy:d", "disabled": true}]}], "http_status": 200}
{"home": "c19-replaced", "channel": "222222222222222222", "message": "700000000000000001", "content": null, "components": [{"type": 1, "components": [{"type": 2, "style": 2, "label": "NorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorth", "custom_id": "fmd:key:route-fix:legacy:1", "disabled": true}, {"type": 2, "style": 2, "label": "South", "custom_id": "fmd:key:route-fix:legacy:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:legacy:d", "disabled": true}]}], "http_status": 200}
{"home": "c21-North", "channel": "222222222222222222", "message": "700000000000000001", "content": null, "components": [{"type": 1, "components": [{"type": 2, "style": 3, "label": "North ✓", "custom_id": "fmd:key:route-fix:historical:1", "disabled": true}, {"type": 2, "style": 2, "label": "South", "custom_id": "fmd:key:route-fix:historical:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:historical:d", "disabled": true}]}], "http_status": 200}
{"home": "c21-South", "channel": "222222222222222222", "message": "700000000000000001", "content": null, "components": [{"type": 1, "components": [{"type": 2, "style": 2, "label": "North", "custom_id": "fmd:key:route-fix:historical:1", "disabled": true}, {"type": 2, "style": 3, "label": "South ✓", "custom_id": "fmd:key:route-fix:historical:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:historical:d", "disabled": true}]}], "http_status": 200}
{"home": "c21-unknown", "channel": "222222222222222222", "message": "700000000000000001", "content": null, "components": [{"type": 1, "components": [{"type": 2, "style": 2, "label": "North", "custom_id": "fmd:key:route-fix:historical:1", "disabled": true}, {"type": 2, "style": 2, "label": "South", "custom_id": "fmd:key:route-fix:historical:2", "disabled": true}]}, {"type": 1, "components": [{"type": 2, "style": 2, "label": "Discuss", "custom_id": "fmd:key:route-fix:historical:d", "disabled": true}]}], "http_status": 200}
Evidence: Outcome headers and button states
[
  {
    "case": "c2",
    "message": "700000000000000001",
    "http_status": 200,
    "text_edit": true,
    "content_length": 167,
    "header": "**⏳ North sent by Dana <t:1791720000:f> - waiting for Firstmate to record it**",
    "buttons": [
      {
        "label": "North ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "South",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c2",
    "message": "700000000000000001",
    "http_status": 200,
    "text_edit": true,
    "content_length": 132,
    "header": "**✅ North chosen by Dana <t:1791720000:f>**",
    "buttons": [
      {
        "label": "North ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "South",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c6",
    "message": "700000000000000007",
    "http_status": 200,
    "text_edit": true,
    "content_length": 119,
    "header": "**✅ Decided <t:1791720000:f>**",
    "buttons": [
      {
        "label": "North",
        "style": 2,
        "disabled": true
      },
      {
        "label": "South",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c6",
    "message": "700000000000000003",
    "http_status": 200,
    "text_edit": true,
    "content_length": 139,
    "header": "**✅ South chosen by the captain <t:1791720000:f>**",
    "buttons": [
      {
        "label": "North",
        "style": 2,
        "disabled": true
      },
      {
        "label": "South ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c6",
    "message": "700000000000000005",
    "http_status": 200,
    "text_edit": true,
    "content_length": 135,
    "header": "**Closed - no longer needed <t:1791720000:f>**",
    "buttons": [
      {
        "label": "North",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c6",
    "message": "700000000000000002",
    "http_status": 200,
    "text_edit": true,
    "content_length": 139,
    "header": "**✅ South chosen by the captain <t:1791720000:f>**",
    "buttons": [
      {
        "label": "North",
        "style": 2,
        "disabled": true
      },
      {
        "label": "South ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c6",
    "message": "700000000000000004",
    "http_status": 200,
    "text_edit": true,
    "content_length": 119,
    "header": "**✅ Decided <t:1791720000:f>**",
    "buttons": [
      {
        "label": "North",
        "style": 2,
        "disabled": true
      },
      {
        "label": "South",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c6",
    "message": "700000000000000001",
    "http_status": 200,
    "text_edit": true,
    "content_length": 132,
    "header": "**✅ North chosen by Dana <t:1791720000:f>**",
    "buttons": [
      {
        "label": "North ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "South",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c6",
    "message": "700000000000000006",
    "http_status": 200,
    "text_edit": true,
    "content_length": 139,
    "header": "**✅ South chosen by the captain <t:1791720000:f>**",
    "buttons": [
      {
        "label": "North",
        "style": 2,
        "disabled": true
      },
      {
        "label": "South ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c20",
    "message": "700000000000000001",
    "http_status": 200,
    "text_edit": false,
    "content_length": null,
    "header": null,
    "buttons": [
      {
        "label": "North",
        "style": 2,
        "disabled": true
      },
      {
        "label": "South ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c7",
    "message": "700000000000000010",
    "http_status": 200,
    "text_edit": true,
    "content_length": 220,
    "header": "♻️ **Replaced by a newer question** ([open](https://discord.com/channels/111111111111111111/222222222222222222/700000000000000011))",
    "buttons": [
      {
        "label": "North",
        "style": 2,
        "disabled": true
      },
      {
        "label": "South",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c7",
    "message": "700000000000000011",
    "http_status": 200,
    "text_edit": true,
    "content_length": 132,
    "header": "**✅ South chosen by Dana <t:1791720000:f>**",
    "buttons": [
      {
        "label": "South ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "North",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c7",
    "message": "700000000000000001",
    "http_status": 200,
    "text_edit": true,
    "content_length": 220,
    "header": "♻️ **Replaced by a newer question** ([open](https://discord.com/channels/111111111111111111/222222222222222222/700000000000000002))",
    "buttons": [
      {
        "label": "North",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c17",
    "message": "700000000000000102",
    "http_status": 200,
    "text_edit": true,
    "content_length": 226,
    "header": "**✅ North chosen by the captain <t:1791720000:f>**",
    "buttons": [
      {
        "label": "North ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "South",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c17",
    "message": "700000000000000112",
    "http_status": 200,
    "text_edit": true,
    "content_length": 2000,
    "header": "**✅ North chosen by the captain <t:1791720000:f>**",
    "buttons": [
      {
        "label": "North ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "South",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c17",
    "message": "700000000000000120",
    "http_status": 200,
    "text_edit": false,
    "content_length": null,
    "header": null,
    "buttons": [
      {
        "label": "NorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNor ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "South",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c17",
    "message": "700000000000000130",
    "http_status": 200,
    "text_edit": true,
    "content_length": 121,
    "header": "**Replaced by a newer question**",
    "buttons": [
      {
        "label": "North",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c19-waiting",
    "message": "700000000000000001",
    "http_status": 200,
    "text_edit": false,
    "content_length": null,
    "header": null,
    "buttons": [
      {
        "label": "NorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNor ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "South",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c20",
    "message": "700000000000000001",
    "http_status": 429,
    "text_edit": false,
    "content_length": null,
    "header": null,
    "buttons": [
      {
        "label": "North",
        "style": 2,
        "disabled": true
      },
      {
        "label": "South ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c20",
    "message": "700000000000000001",
    "http_status": 200,
    "text_edit": false,
    "content_length": null,
    "header": null,
    "buttons": [
      {
        "label": "North",
        "style": 2,
        "disabled": true
      },
      {
        "label": "South ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c19-waiting",
    "message": "700000000000000001",
    "http_status": 200,
    "text_edit": false,
    "content_length": null,
    "header": null,
    "buttons": [
      {
        "label": "NorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNor ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "South",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c19-chosen",
    "message": "700000000000000001",
    "http_status": 200,
    "text_edit": false,
    "content_length": null,
    "header": null,
    "buttons": [
      {
        "label": "NorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNor ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "South",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c19-captain",
    "message": "700000000000000001",
    "http_status": 200,
    "text_edit": false,
    "content_length": null,
    "header": null,
    "buttons": [
      {
        "label": "NorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNor ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "South",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c19-ambiguous",
    "message": "700000000000000001",
    "http_status": 200,
    "text_edit": false,
    "content_length": null,
    "header": null,
    "buttons": [
      {
        "label": "NorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorth",
        "style": 2,
        "disabled": true
      },
      {
        "label": "South",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c19-closed",
    "message": "700000000000000001",
    "http_status": 200,
    "text_edit": false,
    "content_length": null,
    "header": null,
    "buttons": [
      {
        "label": "NorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorth",
        "style": 2,
        "disabled": true
      },
      {
        "label": "South",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c19-replaced",
    "message": "700000000000000001",
    "http_status": 200,
    "text_edit": false,
    "content_length": null,
    "header": null,
    "buttons": [
      {
        "label": "NorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorthNorth",
        "style": 2,
        "disabled": true
      },
      {
        "label": "South",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c21-North",
    "message": "700000000000000001",
    "http_status": 200,
    "text_edit": false,
    "content_length": null,
    "header": null,
    "buttons": [
      {
        "label": "North ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "South",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c21-South",
    "message": "700000000000000001",
    "http_status": 200,
    "text_edit": false,
    "content_length": null,
    "header": null,
    "buttons": [
      {
        "label": "North",
        "style": 2,
        "disabled": true
      },
      {
        "label": "South ✓",
        "style": 3,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  },
  {
    "case": "c21-unknown",
    "message": "700000000000000001",
    "http_status": 200,
    "text_edit": false,
    "content_length": null,
    "header": null,
    "buttons": [
      {
        "label": "North",
        "style": 2,
        "disabled": true
      },
      {
        "label": "South",
        "style": 2,
        "disabled": true
      },
      {
        "label": "Discuss",
        "style": 2,
        "disabled": true
      }
    ]
  }
]

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

🔧 **Rebase** - 1 issue found → auto-fixed ✅
  • ⚠️ docs/configuration.md - merge conflict rebasing onto origin/main

🔧 Fix applied.
✅ Re-checked - no issues remain.

🔧 **Review** - 3 issues found → auto-fixed (5) ✅
  • 🚨 bin/fm-ext-decision.sh:657 - The legacy overflow fallback contradicts “never trim the header, question, or option list.” An existing 1,900-character post plus a long waiting header exceeds 2,000 characters; this path passes the entire original text as the question and deliberately slices it, then appends reconstructed options. Remove this question-trimming fallback and resolve legacy overflow without silently weakening the accepted constraint. Related sites: bin/fm-ext-decision.sh:624 and :641 perform slicing; :657–658 enable it; docs/configuration.md:1022 documents the exception; tests/fm-ext-decision.test.sh:1440–1457 endorses it.
  • 🚨 contrib/hermes-gateway-firstmate-comms/outbox_poster.py:399 - Legacy resume must retain its stored split budget. Here legacy=True still reduces a stored 1,900-character limit to 1,744. A partially posted 3,600-character decision previously split into two chunks can now produce three, making deliver_one return mid-delivery indefinitely because stored total remains two. When counts match, changed boundaries can repeat content. Bypass the new reservation for legacy resumes and pass the original limit unchanged. Related changed call site: contrib/hermes-gateway-firstmate-comms/outbox_poster.py:1830–1835 routes existing progress through this path.
  • 🚨 contrib/hermes-gateway-firstmate-comms/outbox_poster.py:415 - The new structured splitter silently truncates protected question text at the configured chunk cap. With FM_EXT_DISCORD_THREAD_MAX=1, a 1,800-character question plus two short options and the normal cue previously fits the default 1,900-character budget. The new reservation and suffix subtraction reduce the question budget below 1,744; split_discord truncates its tail to an ellipsis, then this function appends options and successfully delivers the incomplete question. Detect cap overflow before publishing and preserve the question, trimming only explanation or refusing an impossible plan. Related changed sites: :398 reserves space, :412 subtracts the suffix, and :418 appends options after truncation.

🔧 Fix applied.
3 errors still open:

  • 🚨 bin/fm-ext-decision.sh:657 - The legacy overflow fallback contradicts “never trim the header, question, or option list.” An existing 1,900-character post plus a long waiting header exceeds 2,000 characters; this path passes the entire original text as the question and deliberately slices it, then appends reconstructed options. Remove this question-trimming fallback and resolve legacy overflow without silently weakening the accepted constraint. Related sites: bin/fm-ext-decision.sh:624 and :641 perform slicing; :657–658 enable it; docs/configuration.md:1022 documents the exception; tests/fm-ext-decision.test.sh:1440–1457 endorses it.
  • 🚨 bin/fm-ext-decision.sh:664 - Completed legacy posts can still lose their questions. Commit 84ec252 introduced persisted texts; older completed progress records lack that field and already have delivery receipts, so they never resume to acquire it. The new .texts[-1] // &#34;&#34; substitutes an empty original, producing and finalizing a header-only edit. This contradicts “above that chunk’s original text unchanged.” Recover the original chunk before editing; never treat missing text as empty. Round 1 fixed legacy resumes but left completed generations uncovered. Related sites: bin/fm-ext-decision.sh:637 accepts the empty original, :683 prepares the replacement; contrib/hermes-gateway-firstmate-comms/outbox_poster.py:1560 publishes it.
  • 🚨 bin/fm-ext-decision.sh:646 - Round 1 replaced legacy overflow truncation with an indefinitely skipped edit: error(&#34;legacy decision text cannot safely fit its outcome header&#34;), followed by || continue at bin/fm-ext-decision.sh:683. A 1,900-character legacy post with a sufficiently long waiting header therefore keeps enabled buttons and receives no outcome header. This contradicts “Every outcome state … must edit” and “Buttons stay visible and disabled.” The prior decision authorized removing question trimming, not omitting settlement display. Resolve the behavior for impossible-to-fit legacy posts explicitly before accepting this fallback. Related sites: docs/configuration.md:1023 documents refusal; tests/fm-ext-decision.test.sh:1451 endorses no edit.

🔧 Fix applied.
5 errors still open:

  • 🚨 bin/fm-ext-decision.sh:657 - The legacy overflow fallback contradicts “never trim the header, question, or option list.” An existing 1,900-character post plus a long waiting header exceeds 2,000 characters; this path passes the entire original text as the question and deliberately slices it, then appends reconstructed options. Remove this question-trimming fallback and resolve legacy overflow without silently weakening the accepted constraint. Related sites: bin/fm-ext-decision.sh:624 and :641 perform slicing; :657–658 enable it; docs/configuration.md:1022 documents the exception; tests/fm-ext-decision.test.sh:1440–1457 endorses it.
  • 🚨 bin/fm-ext-decision.sh:664 - Completed legacy posts can still lose their questions. Commit 84ec252 introduced persisted texts; older completed progress records lack that field and already have delivery receipts, so they never resume to acquire it. The new .texts[-1] // &#34;&#34; substitutes an empty original, producing and finalizing a header-only edit. This contradicts “above that chunk’s original text unchanged.” Recover the original chunk before editing; never treat missing text as empty. Round 1 fixed legacy resumes but left completed generations uncovered. Related sites: bin/fm-ext-decision.sh:637 accepts the empty original, :683 prepares the replacement; contrib/hermes-gateway-firstmate-comms/outbox_poster.py:1560 publishes it.
  • 🚨 bin/fm-ext-decision.sh:646 - Round 1 replaced legacy overflow truncation with an indefinitely skipped edit: error(&#34;legacy decision text cannot safely fit its outcome header&#34;), followed by || continue at bin/fm-ext-decision.sh:683. A 1,900-character legacy post with a sufficiently long waiting header therefore keeps enabled buttons and receives no outcome header. This contradicts “Every outcome state … must edit” and “Buttons stay visible and disabled.” The prior decision authorized removing question trimming, not omitting settlement display. Resolve the behavior for impossible-to-fit legacy posts explicitly before accepting this fallback. Related sites: docs/configuration.md:1023 documents refusal; tests/fm-ext-decision.test.sh:1451 endorses no edit.
  • 🚨 bin/fm-ext-decision.sh:646 - Legacy overflow remains unresolved after rounds 1 and 2. A 1,900-character original plus a long waiting header reaches this error, and command_posts skips the edit. This contradicts “Every outcome state … must edit” and “Buttons stay visible and disabled”: the outcome is absent and buttons remain enabled. Resolve this constraint conflict explicitly; refusing the edit was not the accepted fix. Related sites: bin/fm-ext-decision.sh:687 skips publication; contrib/hermes-gateway-firstmate-comms/outbox_poster.py:1586–1590 repeats the refusal during recovery; docs/configuration.md:1026 documents it; tests/fm-ext-decision.test.sh:1451 endorses it.
  • 🚨 contrib/hermes-gateway-firstmate-comms/outbox_poster.py:1577 - Round 2 leaves already-edited legacy posts behind. A completed legacy post without texts can have a .finalized receipt for its waiting state before upgrade. When Firstmate subsequently records the answer, this new exists() guard permanently skips it, leaving Discord showing “waiting” after the decision is recorded. This contradicts “Every outcome state … must edit” and preserving existing retries. Recover the original last chunk from the retained payload using its stored legacy split budget rather than treating any earlier edit as unrecoverable. Related sites: bin/fm-ext-decision.sh:683–701 emits missing originals for these posts; contrib/hermes-gateway-firstmate-comms/outbox_poster.py:1588–1590 suppresses every subsequent attempt.

🔧 Fix applied.
7 errors still open:

  • 🚨 bin/fm-ext-decision.sh:657 - The legacy overflow fallback contradicts “never trim the header, question, or option list.” An existing 1,900-character post plus a long waiting header exceeds 2,000 characters; this path passes the entire original text as the question and deliberately slices it, then appends reconstructed options. Remove this question-trimming fallback and resolve legacy overflow without silently weakening the accepted constraint. Related sites: bin/fm-ext-decision.sh:624 and :641 perform slicing; :657–658 enable it; docs/configuration.md:1022 documents the exception; tests/fm-ext-decision.test.sh:1440–1457 endorses it.
  • 🚨 bin/fm-ext-decision.sh:664 - Completed legacy posts can still lose their questions. Commit 84ec252 introduced persisted texts; older completed progress records lack that field and already have delivery receipts, so they never resume to acquire it. The new .texts[-1] // &#34;&#34; substitutes an empty original, producing and finalizing a header-only edit. This contradicts “above that chunk’s original text unchanged.” Recover the original chunk before editing; never treat missing text as empty. Round 1 fixed legacy resumes but left completed generations uncovered. Related sites: bin/fm-ext-decision.sh:637 accepts the empty original, :683 prepares the replacement; contrib/hermes-gateway-firstmate-comms/outbox_poster.py:1560 publishes it.
  • 🚨 bin/fm-ext-decision.sh:646 - Round 1 replaced legacy overflow truncation with an indefinitely skipped edit: error(&#34;legacy decision text cannot safely fit its outcome header&#34;), followed by || continue at bin/fm-ext-decision.sh:683. A 1,900-character legacy post with a sufficiently long waiting header therefore keeps enabled buttons and receives no outcome header. This contradicts “Every outcome state … must edit” and “Buttons stay visible and disabled.” The prior decision authorized removing question trimming, not omitting settlement display. Resolve the behavior for impossible-to-fit legacy posts explicitly before accepting this fallback. Related sites: docs/configuration.md:1023 documents refusal; tests/fm-ext-decision.test.sh:1451 endorses no edit.
  • 🚨 bin/fm-ext-decision.sh:646 - Legacy overflow remains unresolved after rounds 1 and 2. A 1,900-character original plus a long waiting header reaches this error, and command_posts skips the edit. This contradicts “Every outcome state … must edit” and “Buttons stay visible and disabled”: the outcome is absent and buttons remain enabled. Resolve this constraint conflict explicitly; refusing the edit was not the accepted fix. Related sites: bin/fm-ext-decision.sh:687 skips publication; contrib/hermes-gateway-firstmate-comms/outbox_poster.py:1586–1590 repeats the refusal during recovery; docs/configuration.md:1026 documents it; tests/fm-ext-decision.test.sh:1451 endorses it.
  • 🚨 contrib/hermes-gateway-firstmate-comms/outbox_poster.py:1577 - Round 2 leaves already-edited legacy posts behind. A completed legacy post without texts can have a .finalized receipt for its waiting state before upgrade. When Firstmate subsequently records the answer, this new exists() guard permanently skips it, leaving Discord showing “waiting” after the decision is recorded. This contradicts “Every outcome state … must edit” and preserving existing retries. Recover the original last chunk from the retained payload using its stored legacy split budget rather than treating any earlier edit as unrecoverable. Related sites: bin/fm-ext-decision.sh:683–701 emits missing originals for these posts; contrib/hermes-gateway-firstmate-comms/outbox_poster.py:1588–1590 suppresses every subsequent attempt.
  • 🚨 bin/fm-ext-decision.sh:646 - Legacy overflow remains unresolved after rounds 1–3. A 1,900-character original plus an 80-character option’s waiting header exceeds 2,000 characters; this error causes command_posts to skip publication, leaving the outcome absent and buttons enabled. This contradicts “Every outcome state … must edit” and “Buttons stay visible and disabled.” Resolve the incompatible legacy constraints explicitly before merging; silent refusal is not the accepted fix. Siblings: bin/fm-ext-decision.sh:687 skips publication; contrib/hermes-gateway-firstmate-comms/outbox_poster.py:1596–1600 repeats the refusal during recovery; docs/configuration.md:1025 documents it; tests/fm-ext-decision.test.sh:1451 endorses it.
  • 🚨 contrib/hermes-gateway-firstmate-comms/outbox_poster.py:1590 - Round 3 leaves previously edited legacy posts without retained payloads permanently stuck. Before upgrade, successful delivery retired the payload; older progress lacks texts, and a waiting edit already created .finalized. After Firstmate records the answer, this guard raises and every reconciliation skips the post, so Discord continues showing “waiting.” New retention cannot restore payloads already deleted. This still contradicts “Every outcome state … must edit” and preserving retries. Establish an explicit migration exception or an available recovery source rather than treating retention as a complete fix. Related changed sites: bin/fm-ext-lib.sh:1512–1516 only retains surviving payloads; outbox_poster.py:1598–1600 suppresses the update; docs/configuration.md:1022–1024 describes the incomplete recovery.

🔧 Fix applied.
6 errors still open:

  • 🚨 bin/fm-ext-decision.sh:657 - The legacy overflow fallback contradicts “never trim the header, question, or option list.” An existing 1,900-character post plus a long waiting header exceeds 2,000 characters; this path passes the entire original text as the question and deliberately slices it, then appends reconstructed options. Remove this question-trimming fallback and resolve legacy overflow without silently weakening the accepted constraint. Related sites: bin/fm-ext-decision.sh:624 and :641 perform slicing; :657–658 enable it; docs/configuration.md:1022 documents the exception; tests/fm-ext-decision.test.sh:1440–1457 endorses it.
  • 🚨 bin/fm-ext-decision.sh:664 - Completed legacy posts can still lose their questions. Commit 84ec252 introduced persisted texts; older completed progress records lack that field and already have delivery receipts, so they never resume to acquire it. The new .texts[-1] // &#34;&#34; substitutes an empty original, producing and finalizing a header-only edit. This contradicts “above that chunk’s original text unchanged.” Recover the original chunk before editing; never treat missing text as empty. Round 1 fixed legacy resumes but left completed generations uncovered. Related sites: bin/fm-ext-decision.sh:637 accepts the empty original, :683 prepares the replacement; contrib/hermes-gateway-firstmate-comms/outbox_poster.py:1560 publishes it.
  • 🚨 bin/fm-ext-decision.sh:646 - Round 1 replaced legacy overflow truncation with an indefinitely skipped edit: error(&#34;legacy decision text cannot safely fit its outcome header&#34;), followed by || continue at bin/fm-ext-decision.sh:683. A 1,900-character legacy post with a sufficiently long waiting header therefore keeps enabled buttons and receives no outcome header. This contradicts “Every outcome state … must edit” and “Buttons stay visible and disabled.” The prior decision authorized removing question trimming, not omitting settlement display. Resolve the behavior for impossible-to-fit legacy posts explicitly before accepting this fallback. Related sites: docs/configuration.md:1023 documents refusal; tests/fm-ext-decision.test.sh:1451 endorses no edit.
  • 🚨 bin/fm-ext-decision.sh:646 - Legacy overflow remains unresolved after rounds 1 and 2. A 1,900-character original plus a long waiting header reaches this error, and command_posts skips the edit. This contradicts “Every outcome state … must edit” and “Buttons stay visible and disabled”: the outcome is absent and buttons remain enabled. Resolve this constraint conflict explicitly; refusing the edit was not the accepted fix. Related sites: bin/fm-ext-decision.sh:687 skips publication; contrib/hermes-gateway-firstmate-comms/outbox_poster.py:1586–1590 repeats the refusal during recovery; docs/configuration.md:1026 documents it; tests/fm-ext-decision.test.sh:1451 endorses it.
  • 🚨 bin/fm-ext-decision.sh:646 - Legacy overflow remains unresolved after rounds 1–3. A 1,900-character original plus an 80-character option’s waiting header exceeds 2,000 characters; this error causes command_posts to skip publication, leaving the outcome absent and buttons enabled. This contradicts “Every outcome state … must edit” and “Buttons stay visible and disabled.” Resolve the incompatible legacy constraints explicitly before merging; silent refusal is not the accepted fix. Siblings: bin/fm-ext-decision.sh:687 skips publication; contrib/hermes-gateway-firstmate-comms/outbox_poster.py:1596–1600 repeats the refusal during recovery; docs/configuration.md:1025 documents it; tests/fm-ext-decision.test.sh:1451 endorses it.
  • 🚨 bin/fm-ext-decision.sh:305 - Round 4's legacy migration leaves previously settled choices dependent on the current lifecycle. Before upgrade, settle a post as North, then reopen its key or re-hold its task. Its immutable settlement has no selected_option_id. This fallback calls post_outcome, which now returns superseded with no answer, so the components-only migration permanently receipts all-grey buttons despite the retained outcome naming North. This contradicts the accepted requirement that legacy posts retain “the chosen one success-styled with a check mark.” Recover the historical selection from the existing immutable settlement evidence rather than the current lifecycle, preserving write-once semantics. Related changed sites: bin/fm-ext-decision.sh:665 consumes the selection; :696 records the empty selection; contrib/hermes-gateway-firstmate-comms/decisions.py:102 renders every option grey.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 8 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Click an option: waiting and recorded outcomes retain the question and highlight the disabled choice. ✅ pass live Captured product PATCH requests: c2
Record an answer elsewhere, an ambiguous answer, or closure: preserve the question and show the appropriate disabled buttons. ✅ pass live Captured product PATCH requests: c6
Replace a question: show the newer message link when known and fallback wording otherwise. ✅ pass live Captured product PATCH requests: c7 and c17
Settle long posts: edit only the final chunk, stay within 2,000 characters, and trim only explanation text. ✅ pass live live-cli-http.log: case 17; Captured product PATCH requests: c17
Settle every legacy outcome with 1,900-character text and an 80-character option: preserve text bytes and disable the buttons. ✅ pass live live-cli-http.log: case 19; Captured product PATCH requests: c19-*
Recover a legacy waiting receipt without original text or payload: retry a failed edit without advancing its receipt, then settle the buttons. ✅ pass live live-http-retry.log; Captured product PATCH requests: c20, HTTP 429 then HTTP 200
Reopen a settled legacy decision: retain its historical highlighted choice, or all-grey buttons when no choice was recorded. ✅ pass live live-cli-http.log: case 21; Captured product PATCH requests: c21-North, c21-South, c21-unknown
Attempt duplicate, stale, mismatched, and plain-text choices: preserve existing refusal and occurrence guards. ✅ pass live live-cli-http.log: cases 6–7; tests/fm-ext-decision.test.sh; tests/fm-ext-ask.test.sh
  • TMPDIR="$PWD/.phase-test-tmp" PYTHONDONTWRITEBYTECODE=1 bash tests/fm-ext-decision.test.sh
  • TMPDIR="$PWD/.phase-test-tmp" PYTHONDONTWRITEBYTECODE=1 bash tests/fm-ext-ask.test.sh
  • Disposable live driver: cases 2, 6, 7, 17, 19, and 21 using the real CLI/poster against a local HTTP endpoint.
  • Disposable legacy-waiting and HTTP-retry drivers: case 20, including an actual HTTP 429 followed by successful retry.
  • Verified cleanup and an unchanged working tree.
✅ **Document** - passed

✅ No issues found.

🔧 **Lint** - 1 issue found → auto-fixed ✅
  • ⚠️ linter found issues (exit code 1)

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

@dnth
dnth merged commit c0ab5ce into main Oct 16, 2026
18 checks passed
@dnth
dnth deleted the fm/fm-ext-decision-keep-question branch October 16, 2026 13:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant