Skip to content

feat(worker): --webhook to POST task transitions to Slack/Discord/generic - #46

Merged
PounceAI merged 3 commits into
mainfrom
feat/worker-webhook
Jul 3, 2026
Merged

feat(worker): --webhook to POST task transitions to Slack/Discord/generic#46
PounceAI merged 3 commits into
mainfrom
feat/worker-webhook

Conversation

@Joshua-Gilbert

Copy link
Copy Markdown
Collaborator

What

Add --webhook <url> to the worker: POST notable task transitions — a task done / blocked / needs-input / retrying, and the worker itself stopping / erroring — to a URL, so an unattended drain can notify Slack, Discord, or a generic receiver without anyone watching the board.

The webhook is a second sink hung off the worker's existing emit() transition point, beside the --emit-json stdout stream. One application/json payload serves all three receivers with no per-target config: text (Slack renders it), content (Discord renders it), and a structured { event, seq, data, worker, ts } for a generic consumer. seq is monotonic, so concurrently-delivered POSTs can be reordered.

Guarantees

Best-effort by construction: a POST never blocks or crashes the drain. A per-request timeout plus an in-flight cap bound resource use against a slow or dead endpoint, and flush() — self-bounded by its own wall clock — drains pending POSTs before every process exit so the final event still lands.

Security

The URL is validated at startup (http/https only, fail loud) and redacted in logs, since a Slack/Discord webhook URL carries its secret in the path. The payload transmits cwd, task titles, and question text, so it's meant only for a trusted endpoint; --webhook-secret <s> HMAC-signs the body (X-Bob-Signature) for a generic receiver to verify.

…eric

Add a --webhook <url> flag that POSTs notable worker transitions (a task
done/blocked/needs-input/retrying, and the worker stopping/erroring) to a URL,
hung off the existing emit() transition point beside --emit-json. One
application/json payload serves three receivers with no per-target config:
text (Slack), content (Discord), and a structured { event, seq, data, worker,
ts } for a generic consumer.

Best-effort by construction: a POST never blocks or crashes the drain, a
per-request timeout plus an in-flight cap bound resource use against a
slow/dead endpoint, and flush() drains pending POSTs before every process exit
so the final event still lands. seq is monotonic so a receiver can reorder the
concurrently-delivered POSTs.

The URL is validated at startup (http/https only) and redacted in logs, since
a Slack/Discord URL carries its secret in the path. --webhook-secret HMAC-signs
the body (X-Bob-Signature) for a generic receiver to verify; the payload
transmits cwd, task titles, and question text, so point it only at a trusted
endpoint.
…it paths

- seq advances only after JSON.stringify succeeds, so a serialization failure
  (e.g. a BigInt in the event data) no longer burns a sequence number and leaves
  a phantom gap a receiver would read as a dropped delivery.
- validateWebhookUrl no longer echoes the URL in its error, and the parse error
  is redacted at the call site, so a rejected but secret-bearing URL can't leak
  to stderr — consistent with the redaction applied everywhere else.
- flush() is a single self-bounded loop: it drains in-flight POSTs but its own
  wall clock (started at flush time) caps the total wait, so a slow endpoint or a
  steady trickle of new POSTs can't hold process exit open. Each round's timer is
  cleared rather than leaked, and there's no longer a dangling drain coroutine.
- the stdin-end shutdown path flushes before exit; the second-Ctrl-C force-quit
  deliberately does not (a force path must not block on network I/O).
An unref'd deadline timer is abandoned when every in-flight POST is a pure
pending promise with no socket to hold the event loop: Node 22 tears the loop
down and flush() never resolves ("Promise resolution is still pending but the
event loop has already resolved"). The timer is cleared right after the race,
so it never outlives the call — unref was both unnecessary and the bug.
@PounceAI
PounceAI merged commit 46ebc6d into main Jul 3, 2026
3 checks passed
@PounceAI
PounceAI deleted the feat/worker-webhook branch July 3, 2026 17:35
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.

2 participants