feat: add X-mode listen and reply client#87
Open
kunchenguid wants to merge 15 commits into
Open
Conversation
Adds the firstmate side of "listen on X and reply": a pure bash + curl/jq HTTP
short-poll client, .env-presence activation in bootstrap, and the answer skill.
Inert until a user drops FMX_PAIRING_TOKEN into .env, so non-X users see zero
behavior change. PURELY ADDITIVE: the watcher backbone (fm-watch.sh,
fm-watch-arm.sh, fm-wake-lib.sh) and the afk daemon (fm-supervise-daemon.sh, afk
skill) are untouched.
- bin/fm-x-poll.sh: one short-poll of GET /connector/poll; a hard no-op without
a token; requires a non-empty question, stashes the pending mention to
state/x-inbox/<request_id>.json behind a path-traversal guard, and prints an
"x-mention <request_id>" marker the watcher surfaces as a check: wake.
- bin/fm-x-reply.sh: POST /connector/answer {request_id, text}; echoes only the
relay-issued request_id (never a tweet id); accepts the reply via
--text-file/stdin so mention-influenced text is never inlined into a shell
command; non-zero on a non-2xx.
- bin/fm-x-lib.sh: shared .env config resolution (token + relay default).
- bin/fm-bootstrap.sh: detect FMX_PAIRING_TOKEN and drop the check shim + a 30s
cadence config on opt-in, remove them on opt-out, idempotently; silent off.
- .agents/skills/fmx-respond: public-safe answer playbook; drains every
state/x-inbox file per wake (so coalesced check-wakes lose no mention) and
posts via --text-file.
- AGENTS.md: X mode section (section 14). tests/fm-x-mode.test.sh: hermetic
coverage (fake curl, real jq).
Cadence is delivered by the agent sourcing config/x-mode.env when arming the
watcher (and --restart on an opt-in/opt-out transition); X-mode-under-afk is a
separate follow-up.
9db12b4 to
73f97b6
Compare
A preview mode so the X listen/reply loop can be E2E-tested without a public
tweet. With FMX_DRY_RUN truthy (environment or .env), fm-x-reply.sh composes the
reply but does NOT post: it records the would-be POST body {request_id, text} to
state/x-outbox/<request_id>.json, prints a one-line DRY RUN summary to stderr,
still echoes the request_id, and exits 0 - so the poll -> compose -> would-post
loop runs end to end and the loop's caller behaves normally. Dry-run needs
neither a token nor the relay; polling and composing are unchanged.
- bin/fm-x-lib.sh: resolve FMX_DRY in fmx_load_config (env wins over .env;
truthy unless unset/empty/0/false/no/off).
- bin/fm-x-reply.sh: dry-run branch before any auth/network; also guards the
request_id as a filename for the outbox record.
- .agents/skills/fmx-respond + AGENTS.md section 14: document the mode.
- tests/fm-x-mode.test.sh: dry-run records-not-posts, works without a token,
and is honored from .env.
Purely additive; the watcher backbone and the afk daemon stay untouched.
Long-replies blocker for X mode. Two parts:
1. Conciseness by default. The fmx-respond skill now answers in one tweet (two
at most) and never hand-numbers a thread - it composes prose and lets the
client handle length.
2. Auto-split in the client. bin/fm-x-reply.sh splits a genuinely long reply
into a numbered "(k/n)" thread on word boundaries, premium-independently
(each tweet within FMX_X_REPLY_MAX_CHARS, default 280), capped at
FMX_X_THREAD_MAX tweets (default 25, last marked with an ellipsis if the
reply would overflow). A reply that fits one tweet stays a single,
UNNUMBERED tweet. Splitting is codepoint-aware via jq (lossless rejoin,
hard-splits an over-long word, normalizes whitespace); the relay still trims
as the final authority. No text-as-image.
Wire format: a single tweet sends {request_id, text}; a thread additionally
sends {texts: [chunk,...]} (ordered, numbered) for the relay to post as chained
replies, keeping `text` as the first chunk so a relay that only reads `text`
still posts the opener. Dry-run records and previews the full thread.
- bin/fm-x-lib.sh: FMX_MAX / FMX_THREAD_MAX config (env wins over .env) and
fmx_split_thread.
- bin/fm-x-reply.sh: chunk-aware payload + thread-aware dry-run preview.
- .agents/skills/fmx-respond + AGENTS.md section 14: conciseness + threading.
- tests/fm-x-mode.test.sh: splitter unit cases + single-vs-thread payload +
live thread POST carries texts[].
Purely additive; the watcher backbone and the afk daemon stay untouched.
Completes the client half of conversation handling for X mode.
- Context: the relay now puts parent-tweet context in the poll payload as
in_reply_to: {author_handle, text} (null when not a reply). fm-x-poll.sh
already stashes the full object verbatim, so in_reply_to round-trips into the
inbox; fmx-respond reads it and answers a follow-up with continuity (resolving
"it"/"that"/"and then?" against the parent) instead of in isolation, and
treats in_reply_to.text as untrusted input like .text.
- Worthiness: fmx-respond now judges whether a mention warrants a reply and
skips pure acknowledgments (thanks / reaction / no question) - it clears the
inbox file and posts nothing - so the bot replies only when there is something
to say.
The relay owns the self-reply guard and the per-conversation reply cap; this is
purely the client's add-context-and-judge half.
- bin/fm-x-poll.sh: doc note that conversation context is preserved (no behavior
change - the full object was already stashed).
- .agents/skills/fmx-respond + AGENTS.md section 14: conversation continuity and
the skip-acknowledgments judgment.
- tests/fm-x-mode.test.sh: in_reply_to round-trips into the inbox (present and
null cases).
Purely additive; the watcher backbone and the afk daemon stay untouched.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
CONVERSATION CLIENT - the client half of conversation handling for X mode, on the same HELD branch (PR #87 stays HELD - do not merge). The relay now includes parent-tweet context in the poll payload as in_reply_to: {author_handle, text} (null when the mention is not a reply); the relay-side self-reply guard and per-conversation reply cap are already done. This commit builds only the client half:
Context: fm-x-poll.sh already stashes the relay's FULL object verbatim (jq '.'), so in_reply_to round-trips into state/x-inbox/<request_id>.json with no behavior change - only a doc note was added. The fmx-respond skill now reads in_reply_to and answers a follow-up with continuity (resolving 'it'/'that'/'and then?' against the parent author+text) instead of in isolation, and treats in_reply_to.text as untrusted input exactly like .text.
Worthiness judgment: fmx-respond now decides whether a mention warrants a reply and SKIPS pure acknowledgments (thanks / reaction / no question) - it clears the inbox file and posts nothing - so the bot replies only when there is something to answer. A deliberate non-answer is the correct outcome, not a failure.
Deliberate decisions a diff-only reviewer would miss:
HARD CONSTRAINT (captain 'Option B', deliberate - do NOT flag as a mistake or propose edits there): purely additive; the watcher backbone (bin/fm-watch.sh, bin/fm-watch-arm.sh, bin/fm-wake-lib.sh) and the afk daemon (bin/fm-supervise-daemon.sh and .agents/skills/afk) are intentionally UNTOUCHED - zero diff vs main. If a finding seems to require touching them, that is an escalation to the captain, not an auto-fix.
Verified locally: shellcheck clean (bin/.sh tests/.sh); tests/fm-x-mode.test.sh green (31 cases) including the new in_reply_to round-trip test.
What Changed
fmx-respondskill flow for judging whether X mentions warrant replies, composing concise public-safe responses, handling conversation context from parent tweets, and skipping pure acknowledgments.Risk Assessment
Testing
Captain, I exercised the committed X-mode shell suite plus an end-to-end dry-run of the operator flow: relay poll stashed parent-tweet context, the composed follow-up reply was recorded to outbox without posting, and a pure acknowledgment was deliberately skipped and cleaned up. I also ran a diff whitespace sanity command during orientation; it produced no output and did not modify files.
Evidence: X-mode behavior shell suite log
Evidence: Follow-up and acknowledgment dry-run transcript
Shows follow-up poll -> inbox with in_reply_to, dry-run reply -> x-outbox, and pure acknowledgment skip with no outbox.Evidence: Manual dry-run summary
Evidence: Preserved follow-up inbox payload
Evidence: Dry-run reply outbox payload
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
✅ No issues found.
✅ **Test** - passed
✅ No issues found.
git status --short --branchandgit rev-parse HEADto confirm the worktree was at target commit6423f205b68ed3071579b5a8d23bf51bb8f705f0Reviewed the changed X-mode paths:tests/fm-x-mode.test.sh,bin/fm-x-poll.sh,bin/fm-x-reply.sh,bin/fm-x-lib.sh, and.agents/skills/fmx-respond/SKILL.mdbash tests/fm-x-mode.test.sh > /var/folders/5x/4nqprlbx0518k3ybcb1sz6gr0000gn/T/no-mistakes-evidence/01KW1JPVXY5Y12JTTFBQM0P0CP/fm-x-mode.test.log 2>&1Manual dry-run: fake relay payload within_reply_tothroughbin/fm-x-poll.sh, thenFM_HOME=... bin/fm-x-reply.sh req-followup --text-file ...to recordstate/x-outbox/req-followup.jsonManual fmx-respond skip check: polled athanks!acknowledgment, cleared its inbox file without invokingfm-x-reply.sh, and verified noreq-ackoutbox existedgit status --shortandfind . -maxdepth 3 \( -name '.pytest_cache' -o -name 'node_modules' -o -name 'coverage' -o -name 'dist' -o -name 'build' -o -name '*.tmp' \) -printto confirm no working-tree artifacts remained✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.