Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions bin/fm-discord-notify-status.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}"
FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}"
# shellcheck source=bin/fm-classify-lib.sh
. "$SCRIPT_DIR/fm-classify-lib.sh"
# shellcheck source=bin/fm-discord-lib.sh
. "$SCRIPT_DIR/fm-discord-lib.sh"

# Push a plain one-line message to this home's configured Discord channel,
# reusing fm-discord-notify.sh --report's send path. Silent no-op when Discord
# is not configured (fm-discord-notify.sh --report already fails loudly only
# on a missing token, so the token check here keeps this path a quiet no-op
# instead of an error on a Discord-less home).
fm_discord_send_plain_report() {
local message=$1 channel_id
fm_discord_load_config
[ -n "${FM_DISCORD_TOKEN:-}" ] || return 0
channel_id=$(fm_discord_trim "${FM_DISCORD_CHANNELS%%,*}")
case "$channel_id" in ''|*[!0-9]*) return 0 ;; esac
"$SCRIPT_DIR/fm-discord-notify.sh" --report "$channel_id" "$message"
}

[ "$#" -eq 2 ] || exit 2
task_id=$1
Expand Down Expand Up @@ -61,5 +77,20 @@ case "$verb:$key" in
"$SCRIPT_DIR/fm-discord-notify.sh" pr-ready "$route_task_id" "$key" \
"$summary" "병합|열어 두기" "$task_id"
;;
done:*)
note=$(status_line_note "$line")
note=$(printf '%s' "$note" | tr '\n\r' ' ')
fm_discord_send_plain_report "작업 완료 [$task_id]: ${note:-완료}"
;;
blocked:*)
note=$(status_line_note "$line")
note=$(printf '%s' "$note" | tr '\n\r' ' ')
fm_discord_send_plain_report "확인 필요(막힘) [$task_id]: ${note:-원인 미기재}"
;;
failed:*)
note=$(status_line_note "$line")
note=$(printf '%s' "$note" | tr '\n\r' ' ')
fm_discord_send_plain_report "작업 실패 [$task_id]: ${note:-원인 미기재}"
;;
*) exit 0 ;;
esac
7 changes: 4 additions & 3 deletions bin/fm-watch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1929,9 +1929,10 @@ signal_files_actionable() { # <status-file> ...
return "$found"
}

# Push only the two additional captain-facing transitions that originate in
# worker status logs. Captain holds and local PR registration publish directly
# at their durable mutation sites, where their identities are authoritative.
# Push the captain-facing transitions that originate in worker status logs:
# yolo-off PR-ready, plus done/blocked/failed plain reports. Captain holds and
# local PR registration publish directly at their durable mutation sites,
# where their identities are authoritative.
signal_discord_decision_notifications() { # <status-file> ...
local f start size chunk line task verb key
for f in "$@"; do
Expand Down
2 changes: 1 addition & 1 deletion docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ The shared no-mistakes gate refusal for fleet lifecycle entrypoints is summarize
| `fm-discord-lib.sh` | Shared self-hosted Discord configuration and channel selection |
| `fm-discord-notify.sh` | Push a keyed captain decision or requested report to the configured self-hosted Discord channel and retry pending sends |
| `fm-discord-report.sh` | Send the bounded fleet snapshot to Discord when quiet mode is active and the captain asks for a report |
| `fm-discord-notify-status.sh` | Map an ask-user gate or yolo-off PR-ready status line to a Discord decision notification |
| `fm-discord-notify-status.sh` | Map an ask-user gate, yolo-off PR-ready, or done/blocked/failed status line to a Discord decision notification or plain report |
| `fm-discord-notify.js` | Persist and deliver self-hosted Discord decision notifications through the REST API |
| `fm-discord-poll.sh` | Invoke the bounded self-hosted Discord poll for mentions and authorized decision replies |
| `fm-discord-poll.js` | Poll Discord messages, capture authorized decision replies, and emit inbox wakes |
Expand Down
66 changes: 66 additions & 0 deletions tests/fm-discord-decision-push.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,69 @@ test_pr_push_names_gitlab_project() {
pass "PR-ready notifications name the project for an accepted GitLab merge-request URL too"
}

test_done_status_sends_plain_report() {
local home log body
home="$TMP_ROOT/done-status"
mkdir -p "$home/state/x-context"
chmod 700 "$home/state" "$home/state/x-context"
make_fake_node "$home"
log="$home/posts.jsonl"
FM_TEST_REAL_NODE=$(command -v node) FM_DISCORD_FAKE_POST_LOG="$log" \
PATH="$home/fake-bin:$BASE_PATH" FM_HOME="$home" FM_STATE_OVERRIDE="$home/state" \
FM_DISCORD_BOT_TOKEN=fake-token FM_DISCORD_CHANNEL_ID=1000000000000000001 \
"$ROOT/bin/fm-discord-notify-status.sh" task-a \
'done: wired up the new endpoint' >/dev/null \
|| fail "done status classification failed"
assert_equals "1" "$(wc -l < "$log" | tr -d '[:space:]')" "done status sends exactly one report"
assert_contains "$(jq -r '.url' "$log")" "1000000000000000001" "done status sends to configured channel"
body=$(jq -r '.payload.content' "$log")
assert_contains "$body" "task-a" "done report includes task id"
assert_contains "$body" "wired up the new endpoint" "done report includes the note text"
pass "a done status line sends a plain Discord report with the worker's note"
}

test_blocked_status_sends_plain_report() {
local home log body
home="$TMP_ROOT/blocked-status"
mkdir -p "$home/state/x-context"
chmod 700 "$home/state" "$home/state/x-context"
make_fake_node "$home"
log="$home/posts.jsonl"
FM_TEST_REAL_NODE=$(command -v node) FM_DISCORD_FAKE_POST_LOG="$log" \
PATH="$home/fake-bin:$BASE_PATH" FM_HOME="$home" FM_STATE_OVERRIDE="$home/state" \
FM_DISCORD_BOT_TOKEN=fake-token FM_DISCORD_CHANNEL_ID=1000000000000000001 \
"$ROOT/bin/fm-discord-notify-status.sh" task-b \
'blocked: waiting on API credentials' >/dev/null \
|| fail "blocked status classification failed"
assert_equals "1" "$(wc -l < "$log" | tr -d '[:space:]')" "blocked status sends exactly one report"
assert_contains "$(jq -r '.url' "$log")" "1000000000000000001" "blocked status sends to configured channel"
body=$(jq -r '.payload.content' "$log")
assert_contains "$body" "task-b" "blocked report includes task id"
assert_contains "$body" "waiting on API credentials" "blocked report includes the note text"
pass "a blocked status line sends a plain Discord report with the worker's note"
}

test_failed_status_sends_plain_report() {
local home log body
home="$TMP_ROOT/failed-status"
mkdir -p "$home/state/x-context"
chmod 700 "$home/state" "$home/state/x-context"
make_fake_node "$home"
log="$home/posts.jsonl"
FM_TEST_REAL_NODE=$(command -v node) FM_DISCORD_FAKE_POST_LOG="$log" \
PATH="$home/fake-bin:$BASE_PATH" FM_HOME="$home" FM_STATE_OVERRIDE="$home/state" \
FM_DISCORD_BOT_TOKEN=fake-token FM_DISCORD_CHANNEL_ID=1000000000000000001 \
"$ROOT/bin/fm-discord-notify-status.sh" task-c \
'failed: build script exited 1' >/dev/null \
|| fail "failed status classification failed"
assert_equals "1" "$(wc -l < "$log" | tr -d '[:space:]')" "failed status sends exactly one report"
assert_contains "$(jq -r '.url' "$log")" "1000000000000000001" "failed status sends to configured channel"
body=$(jq -r '.payload.content' "$log")
assert_contains "$body" "task-c" "failed report includes task id"
assert_contains "$body" "build script exited 1" "failed report includes the note text"
pass "a failed status line sends a plain Discord report with the worker's note"
}

test_no_token_is_inert
test_quiet_report_posts_plain_snapshot
test_report_requires_token
Expand All @@ -550,3 +613,6 @@ test_ask_user_gate_alone_triggers_no_push
test_ask_user_escalation_hold_carries_finding_text
test_pr_push_requires_yolo_off
test_pr_push_names_gitlab_project
test_done_status_sends_plain_report
test_blocked_status_sends_plain_report
test_failed_status_sends_plain_report
Loading