Skip to content

fix: require Discord topic thread titles and explicit reply states - #282

Merged
dnth merged 2 commits into
mainfrom
fm/fm-ext-emit-require-title-state
Oct 28, 2026
Merged

dnth merged 2 commits into
mainfrom
fm/fm-ext-emit-require-title-state

Conversation

@dnth

@dnth dnth commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Summary

bin/fm-ext-emit.sh now refuses the two thread mistakes instead of relying on the ext-respond skill being remembered.

 fm-ext-emit.sh
   parse flags, idempotency exits, choice guard
+  if kind in {answer, final} and no --thread-state
+    die "an <kind> must pass --thread-state needs-you|done|none"
   destination context
+  if no --thread-title and the poster would create the thread
+     (origin message, thread == channel, no recorded topic thread, dedicated channel)
+    die "this reply creates the request's topic thread; pass --thread-title \"<Project> · <topic>\""
   publish payload
  • "Would create the thread" mirrors the poster's ensure_topic_thread conditions. The dedicated-channel lookup moved into a shared fm_ext_dedicated_channel in bin/fm-ext-lib.sh, so emit, fm-ext-open.sh, and the poster read the same config.
  • fm-ext-open.sh passes --thread-state none by default on its first message, because that message is an answer. fm-ext-ask.sh already passes needs-you, so it is unchanged.
  • ack and followup still need no state.
  • The ext-respond skill and the topic-thread section of docs/configuration.md state the rule once and point to the emit header as its owner.
  • Test callers pass explicit states. Tests that cover the poster's naming of legacy untitled payloads go through an emit_reply helper that strips a stand-in title.

Evidence

  • Before: an untitled ack on a top-level dedicated-channel request, or an answer/final with no state, published a payload. The thread kept the raw message text as its name and no marker was set.
  • After: new test 34 emit requires a creating title and an answer or final state in tests/fm-ext-topic-threads.test.sh:
untitled thread-creating ack     -> refused, one line, names --thread-title and "<Project> · <topic>", no payload
answer/final without state       -> refused, one line, names --thread-state needs-you|done|none, no payload
titled ack, no state             -> posts
answer --thread-state none       -> posts, payload records "none"
untitled followup/final into existing thread, in-thread request, outside dedicated channel -> post
  • Locally: bin/fm-lint.sh rc=0. fm-ext-topic-threads (34/34), fm-ext-ask, fm-ext-media, fm-ext-decision, fm-ext-inbox-remove, fm-ext-plugin-dest, fm-ext-thread-context, fm-ext-waiting-pin, and fm-ext-ack-guard pass.
  • Two suites failed locally with identical failures on unchanged base code: fm-ext-bridge test 35 (wake row presentation) and fm-ext-watch (mid-session arm timing under host load ~60). They pass in CI on main and on this PR. Neither touches emit.
  • CI: 18/18 checks pass, including every behavior shard.
  • An earlier validation run, 01M6148GQCM2C1A6YVDH55FP66, was cancelled by the supervisor after it stalled in lint. Its document-step commit cc8414cd is kept, and this run validated that head.

Merge Danger

Door: two-way

Reverting restores optional flags. Already-queued payloads are unaffected, because the poster still names untitled legacy payloads.

Blast Radius: Discord replies

Any agent or script that calls fm-ext-emit.sh with an answer/final and no state, or with an untitled thread-creating reply, now gets a refusal and must retry with the flag. Every in-repo caller is updated. Out-of-repo callers would see the one-line refusal.

Testing

Live CLI scenarios and both focused regression files passed; base-version emit reproduced the omissions. Captured CLI transcripts and persisted payloads. Discord transport was injected in regression tests; no real Discord service was contacted. Lint and the full suite were left to their assigned phases.

  • Live validation: ✅ go - 7 of 7 scenarios driven live against the product
Scenario Result Live Evidence
Submit any reply kind that would create a topic thread without a title; receive a one-line refusal naming --thread-title and its format, with no payload published. ✅ pass live Live CLI refusals, acceptance, routing, ask and open
Submit answer/final without a state or with an invalid state; receive a refusal and publish nothing. ✅ pass live Live CLI refusals, acceptance, routing, ask and open
Queue titled ack/followup without state and answer/final with none, needs-you, or done; persisted payloads retain the requested state. ✅ pass live Live CLI refusals, acceptance, routing, ask and open
Retry queued and receipted identities without required flags; succeed without changing existing artifacts or creating another payload. ✅ pass live Live CLI refusals, acceptance, routing, ask and open; Receipt-based idempotent retries
Queue untitled replies for recorded topic threads, in-thread messages, slash requests, and messages outside the dedicated channel. ✅ pass live Live CLI refusals, acceptance, routing, ask and open
Open titled topics without a state and with an explicit override; queue answers with none and needs-you respectively. ✅ pass live Live CLI refusals, acceptance, routing, ask and open
Ask a choice through fm-ext-ask; hold the disposable task and queue matching buttons with needs-you. ✅ pass live Live CLI refusals, acceptance, routing, ask and open
Evidence: Live CLI refusals, acceptance, routing, ask and open
$ bin/fm-ext-intake.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444 --guild-id 111111111111111111 --channel-id 222222222222222222 --thread-id 222222222222222222 --message-id 444444444444444444 --author 555555555555555555 --secret-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/config/ext-secret --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt --origin message --no-wake
exit=0
2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22

SCENARIO: refuse creating replies without title, all reply kinds
$ bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444 --kind ack --generation 1 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt
exit=2
fm-ext-emit: this reply creates the request's topic thread; pass --thread-title "<Project> · <topic>"

$ bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444 --kind followup --generation 1 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt
exit=2
fm-ext-emit: this reply creates the request's topic thread; pass --thread-title "<Project> · <topic>"

$ bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444 --kind answer --generation 1 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt --thread-state none
exit=2
fm-ext-emit: this reply creates the request's topic thread; pass --thread-title "<Project> · <topic>"

$ bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444 --kind final --generation 1 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt --thread-state none
exit=2
fm-ext-emit: this reply creates the request's topic thread; pass --thread-title "<Project> · <topic>"

SCENARIO: refuse answer/final without explicit state and reject invalid state
$ bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444 --kind answer --generation 1 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt --thread-title Aceh · ingestion
exit=2
fm-ext-emit: an answer must pass --thread-state needs-you|done|none

$ bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444 --kind final --generation 1 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt --thread-title Aceh · ingestion
exit=2
fm-ext-emit: an final must pass --thread-state needs-you|done|none

$ bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444 --kind answer --generation 1 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt --thread-title Aceh · ingestion --thread-state invalid
exit=2
fm-ext-emit: thread state must be needs-you, done, or none

SCENARIO: titled ack/followup omit state; answer/final accept all explicit states
$ bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444 --kind ack --generation 1 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt --thread-title Aceh · ingestion
exit=0
2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22

persisted outbox payload: {"schema_version": 1, "request_id": "discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444", "slug": "2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22", "kind": "ack", "generation": 1, "platform": "discord", "source": "hermes-gateway", "guild_id": "111111111111111111", "channel_id": "222222222222222222", "thread_id": "222222222222222222", "message_id": "444444444444444444", "origin": "message", "text": "Ingestion is progressing.", "recorded_at": 1793189522, "thread_title": "Aceh · ingestion"}
$ bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444 --kind followup --generation 1 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt --thread-title Aceh · ingestion
exit=0
2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22

persisted outbox payload: {"schema_version": 1, "request_id": "discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444", "slug": "2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22", "kind": "followup", "generation": 1, "platform": "discord", "source": "hermes-gateway", "guild_id": "111111111111111111", "channel_id": "222222222222222222", "thread_id": "222222222222222222", "message_id": "444444444444444444", "origin": "message", "text": "Ingestion is progressing.", "recorded_at": 1793189522, "thread_title": "Aceh · ingestion"}
$ bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444 --kind answer --generation 1 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt --thread-title Aceh · ingestion --thread-state none
exit=0
2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22

persisted outbox payload: {"schema_version": 1, "request_id": "discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444", "slug": "2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22", "kind": "answer", "generation": 1, "platform": "discord", "source": "hermes-gateway", "guild_id": "111111111111111111", "channel_id": "222222222222222222", "thread_id": "222222222222222222", "message_id": "444444444444444444", "origin": "message", "text": "Ingestion is progressing.", "recorded_at": 1793189523, "thread_title": "Aceh · ingestion", "thread_state": "none"}
$ bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444 --kind answer --generation 2 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt --thread-title Aceh · ingestion --thread-state needs-you
exit=0
2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22

persisted outbox payload: {"schema_version": 1, "request_id": "discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444", "slug": "2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22", "kind": "answer", "generation": 2, "platform": "discord", "source": "hermes-gateway", "guild_id": "111111111111111111", "channel_id": "222222222222222222", "thread_id": "222222222222222222", "message_id": "444444444444444444", "origin": "message", "text": "Ingestion is progressing.", "recorded_at": 1793189524, "thread_title": "Aceh · ingestion", "thread_state": "needs-you"}
$ bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444 --kind answer --generation 3 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt --thread-title Aceh · ingestion --thread-state done
exit=0
2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22

persisted outbox payload: {"schema_version": 1, "request_id": "discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444", "slug": "2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22", "kind": "answer", "generation": 3, "platform": "discord", "source": "hermes-gateway", "guild_id": "111111111111111111", "channel_id": "222222222222222222", "thread_id": "222222222222222222", "message_id": "4

... [5633 bytes truncated] ...

33333333333333:444444444444444445 --guild-id 111111111111111111 --channel-id 222222222222222222 --thread-id 333333333333333333 --message-id 444444444444444445 --author 555555555555555555 --secret-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/config/ext-secret --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt --origin message --no-wake
exit=0
51991d9b3cb28274d63d6646b9a85281c0ea5b7788bed4bb8d2bc09d07a6c124

$ bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:333333333333333333:444444444444444445 --kind ack --generation 1 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt
exit=0
51991d9b3cb28274d63d6646b9a85281c0ea5b7788bed4bb8d2bc09d07a6c124

persisted outbox payload: {"schema_version": 1, "request_id": "discord:111111111111111111:222222222222222222:333333333333333333:444444444444444445", "slug": "51991d9b3cb28274d63d6646b9a85281c0ea5b7788bed4bb8d2bc09d07a6c124", "kind": "ack", "generation": 1, "platform": "discord", "source": "hermes-gateway", "guild_id": "111111111111111111", "channel_id": "222222222222222222", "thread_id": "333333333333333333", "message_id": "444444444444444445", "origin": "message", "text": "Ingestion is progressing.", "recorded_at": 1793189530}
$ bin/fm-ext-intake.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444446 --guild-id 111111111111111111 --channel-id 222222222222222222 --thread-id 222222222222222222 --message-id 444444444444444446 --author 555555555555555555 --secret-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/config/ext-secret --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt --origin slash --no-wake
exit=0
b6f7eabaaaef80deea63b756da8fedbfa2e3da569a347dd2a70948663387013a

$ bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444446 --kind ack --generation 1 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt
exit=0
b6f7eabaaaef80deea63b756da8fedbfa2e3da569a347dd2a70948663387013a

persisted outbox payload: {"schema_version": 1, "request_id": "discord:111111111111111111:222222222222222222:222222222222222222:444444444444444446", "slug": "b6f7eabaaaef80deea63b756da8fedbfa2e3da569a347dd2a70948663387013a", "kind": "ack", "generation": 1, "platform": "discord", "source": "hermes-gateway", "guild_id": "111111111111111111", "channel_id": "222222222222222222", "thread_id": "222222222222222222", "message_id": "444444444444444446", "origin": "slash", "text": "Ingestion is progressing.", "recorded_at": 1793189532}
$ bin/fm-ext-intake.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444447 --guild-id 111111111111111111 --channel-id 222222222222222222 --thread-id 222222222222222222 --message-id 444444444444444447 --author 555555555555555555 --secret-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/config/ext-secret --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt --origin message --no-wake
exit=0
5ee8f9d7f62129ed21b131e309ac2dec161c545c3c6963e72abb8d98018ed3d2

$ bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444447 --kind ack --generation 1 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt
exit=0
5ee8f9d7f62129ed21b131e309ac2dec161c545c3c6963e72abb8d98018ed3d2

persisted outbox payload: {"schema_version": 1, "request_id": "discord:111111111111111111:222222222222222222:222222222222222222:444444444444444447", "slug": "5ee8f9d7f62129ed21b131e309ac2dec161c545c3c6963e72abb8d98018ed3d2", "kind": "ack", "generation": 1, "platform": "discord", "source": "hermes-gateway", "guild_id": "111111111111111111", "channel_id": "222222222222222222", "thread_id": "222222222222222222", "message_id": "444444444444444447", "origin": "message", "text": "Ingestion is progressing.", "recorded_at": 1793189534}
SCENARIO: open queues answer with default none and explicit override
$ bin/fm-ext-open.sh --key default --thread-title Aceh · ingestion --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt
exit=0
discord:111111111111111111:222222222222222222:222222222222222222:121331711622172781

persisted outbox payload: {"schema_version": 1, "request_id": "discord:111111111111111111:222222222222222222:222222222222222222:121331711622172781", "slug": "8349019f082aed1dc52318fd23a04adbf6d5f7ed59ed517abf1172269d82f718", "kind": "answer", "generation": 1, "platform": "discord", "source": "hermes-gateway", "guild_id": "111111111111111111", "channel_id": "222222222222222222", "thread_id": "222222222222222222", "message_id": "121331711622172781", "origin": "open", "text": "Ingestion is progressing.", "recorded_at": 1793189535, "thread_title": "Aceh · ingestion", "thread_state": "none"}
$ bin/fm-ext-open.sh --key default --lookup
exit=0
discord:111111111111111111:222222222222222222:222222222222222222:121331711622172781
pending

$ bin/fm-ext-open.sh --key waiting --thread-title Aceh · ingestion --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt --thread-state needs-you
exit=0
discord:111111111111111111:222222222222222222:222222222222222222:961588195652322821

persisted outbox payload: {"schema_version": 1, "request_id": "discord:111111111111111111:222222222222222222:222222222222222222:961588195652322821", "slug": "5a32641253931d76e23bee4fd9c54c9b30ff52c17329807d6e5967529c08cf93", "kind": "answer", "generation": 1, "platform": "discord", "source": "hermes-gateway", "guild_id": "111111111111111111", "channel_id": "222222222222222222", "thread_id": "222222222222222222", "message_id": "961588195652322821", "origin": "open", "text": "Ingestion is progressing.", "recorded_at": 1793189538, "thread_title": "Aceh · ingestion", "thread_state": "needs-you"}
$ bin/fm-ext-open.sh --key waiting --lookup
exit=0
discord:111111111111111111:222222222222222222:222222222222222222:961588195652322821
pending

SCENARIO: ask queues needs-you with matching buttons and holds a disposable task
$ bin/fm-ext-ask.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444 --kind answer --generation 4 --task validation-route --title Pick the route --repo sample --reason route pending --mode done --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt --option North --option South
exit=0
2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22
validation-route

persisted outbox payload: {"schema_version": 1, "request_id": "discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444", "slug": "2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22", "kind": "answer", "generation": 4, "platform": "discord", "source": "hermes-gateway", "guild_id": "111111111111111111", "channel_id": "222222222222222222", "thread_id": "222222222222222222", "message_id": "444444444444444444", "origin": "message", "text": "Ingestion is progressing.\n\n1. North\n2. South\nTap a button, or reply with the number.", "recorded_at": 1793189544, "thread_state": "needs-you", "decision": {"key": "hold:validation-route", "mode": "done", "occurrence": "2026-10-28T12:12:19Z#0", "sections": {"question": "Ingestion is progressing.", "explanation": "", "options": "1. North\n2. South", "cue": "Tap a button, or reply with the number.", "structured": true}, "options": [{"id": 1, "label": "North", "confirm": false}, {"id": 2, "label": "South", "confirm": false}], "text": "Ingestion is progressing.\n\n1. North\n2. South\nTap a button, or reply with the number."}}
ALL LIVE CLI SCENARIOS PASSED
Evidence: Receipt-based idempotent retries
$ bin/fm-ext-outbox.sh receipt --slug 2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22 --kind answer --generation 1 --receipt-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/answer-receipt.json
exit=0
receipted 2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22 answer 1

$ bin/fm-ext-outbox.sh receipt --slug 2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22 --kind final --generation 1 --receipt-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/final-receipt.json
exit=0
receipted 2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22 final 1

$ bin/fm-ext-outbox.sh pending
exit=0
~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/state/ext-outbox/2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22.ack.1.json
~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/state/ext-outbox/2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22.answer.2.json
~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/state/ext-outbox/2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22.answer.3.json
~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/state/ext-outbox/2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22.answer.4.json
~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/state/ext-outbox/2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22.final.2.json
~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/state/ext-outbox/2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22.final.3.json
~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/state/ext-outbox/2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22.followup.1.json
~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/state/ext-outbox/2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22.followup.2.json
~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/state/ext-outbox/51991d9b3cb28274d63d6646b9a85281c0ea5b7788bed4bb8d2bc09d07a6c124.ack.1.json
~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/state/ext-outbox/5a32641253931d76e23bee4fd9c54c9b30ff52c17329807d6e5967529c08cf93.answer.1.json
~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/state/ext-outbox/5ee8f9d7f62129ed21b131e309ac2dec161c545c3c6963e72abb8d98018ed3d2.ack.1.json
~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/state/ext-outbox/8349019f082aed1dc52318fd23a04adbf6d5f7ed59ed517abf1172269d82f718.answer.1.json
~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/state/ext-outbox/b6f7eabaaaef80deea63b756da8fedbfa2e3da569a347dd2a70948663387013a.ack.1.json

$ bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444 --kind answer --generation 1 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt
exit=0
2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22

$ bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444 --kind final --generation 1 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/live/text.txt
exit=0
2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22

Receipted answer/final retries omitted both required flags, succeeded, and created no new payload.
Evidence: Base emit reproduces missing-flag omissions
2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22
BASE COMMIT CLI: bash ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/base/bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444 --kind ack --generation 1 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/baseline-home/text
exit=0
2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22

base persisted payload: {"schema_version": 1, "request_id": "discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444", "slug": "2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22", "kind": "ack", "generation": 1, "platform": "discord", "source": "hermes-gateway", "guild_id": "111111111111111111", "channel_id": "222222222222222222", "thread_id": "222222222222222222", "message_id": "444444444444444444", "origin": "message", "text": "Ingestion is progressing.", "recorded_at": 1793189583}
BASE COMMIT CLI: bash ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/base/bin/fm-ext-emit.sh --request-id discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444 --kind answer --generation 1 --text-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M616JK5Q9620S320WNT6TKJV/.phase-test-tmp/baseline-home/text --thread-title Aceh · ingestion
exit=0
2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22

base persisted payload: {"schema_version": 1, "request_id": "discord:111111111111111111:222222222222222222:222222222222222222:444444444444444444", "slug": "2fdeeeec40eba18cfc9a36b53885b48edcf03e3c024a8dba42c9c42a6eb0dc22", "kind": "answer", "generation": 1, "platform": "discord", "source": "hermes-gateway", "guild_id": "111111111111111111", "channel_id": "222222222222222222", "thread_id": "222222222222222222", "message_id": "444444444444444444", "origin": "message", "text": "Ingestion is progressing.", "recorded_at": 1793189584, "thread_title": "Aceh · ingestion"}
Baseline reproduced both missing-flag omissions; target refuses both.
  • Evidence: Focused topic-thread regression tests (local file: ~/.no-mistakes/evidence/01M616JK5Q9620S320WNT6TKJV/topic-tests.log)
Evidence: Focused ask regression tests
route-fix
route-fix
route-fix
ok - 1 fm-ext-ask holds the call, writes the decision, and posts buttons that match the numbered text
idle-call
queued-ship
steer-skipped: queued-ship (no live owning task in this home)
ok - 2 --mode is required, release keeps a held queued ship open, and leaking text or too many options are refused before holding
ok - 3 emit refuses a choice-shaped post without a decision file and names fm-ext-ask
ok - 4 --no-buttons posts a plain list and records its reason
ok - 5 status prose and numbered lists without a choice cue or heading post unchanged
ok - 6 a choice posted with a decision file is unaffected by the guard
retry-answer-done
steer-skipped: retry-answer-done (no live owning task in this home)
retry-answer-release
steer-skipped: retry-answer-release (no live owning task in this home)
retry-followup-done
steer-skipped: retry-followup-done (no live owning task in this home)
retry-followup-release
steer-skipped: retry-followup-release (no live owning task in this home)
retry-final-done
steer-skipped: retry-final-done (no live owning task in this home)
retry-final-release
steer-skipped: retry-final-release (no live owning task in this home)
ok - 7 settled asks preserve task state across payload, posting, and receipt retries
ok - 8 cue verbs and nouns match whole words while genuine choices remain guarded
ok - 9 plural options, choices, numbers, codes, and letters require buttons for every guarded post kind
ok - 10 digit choice cues and choice headings require buttons unless --no-buttons is explicit
ok - 11 bold choice headings require buttons with colons inside, outside, or omitted
ok - 12 force-push flags confirm with intervening targets while ordinary pushes take one click

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 7 of 7 scenarios driven live against the product
Scenario Result Live Evidence
Submit any reply kind that would create a topic thread without a title; receive a one-line refusal naming --thread-title and its format, with no payload published. ✅ pass live Live CLI refusals, acceptance, routing, ask and open
Submit answer/final without a state or with an invalid state; receive a refusal and publish nothing. ✅ pass live Live CLI refusals, acceptance, routing, ask and open
Queue titled ack/followup without state and answer/final with none, needs-you, or done; persisted payloads retain the requested state. ✅ pass live Live CLI refusals, acceptance, routing, ask and open
Retry queued and receipted identities without required flags; succeed without changing existing artifacts or creating another payload. ✅ pass live Live CLI refusals, acceptance, routing, ask and open; Receipt-based idempotent retries
Queue untitled replies for recorded topic threads, in-thread messages, slash requests, and messages outside the dedicated channel. ✅ pass live Live CLI refusals, acceptance, routing, ask and open
Open titled topics without a state and with an explicit override; queue answers with none and needs-you respectively. ✅ pass live Live CLI refusals, acceptance, routing, ask and open
Ask a choice through fm-ext-ask; hold the disposable task and queue matching buttons with needs-you. ✅ pass live Live CLI refusals, acceptance, routing, ask and open
  • TMPDIR="$PWD/.phase-test-tmp" PYTHONDONTWRITEBYTECODE=1 bash tests/fm-ext-topic-threads.test.sh
  • TMPDIR="$PWD/.phase-test-tmp" PYTHONDONTWRITEBYTECODE=1 bash tests/fm-ext-ask.test.sh
  • python3 .phase-test-tmp/drive.py — real intake, emit, open, ask and task-hold interfaces against an isolated home; driver preserved as live-cli-driver.py in the evidence directory.
  • python3 .phase-test-tmp/receipt.py — record receipts through fm-ext-outbox, retire payloads, then retry emit without required flags.
  • python3 .phase-test-tmp/baseline.py — execute the base-version emit CLI and reproduce both missing-flag omissions.
  • Removed the disposable setup and verified git status --short was empty.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

dnth added 2 commits October 28, 2026 18:27
A reply that creates its request's topic thread must pass --thread-title,
and every answer or final must pass an explicit --thread-state; emit
refuses either omission with a one-line message. Callers, tests, the
ext-respond skill, and the topic-thread contract follow.
@dnth
dnth merged commit 518ba0f into main Oct 28, 2026
19 checks passed
@dnth
dnth deleted the fm/fm-ext-emit-require-title-state branch October 28, 2026 13:59
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