Inspiration

Work doesn't get dropped because people don't care. It gets dropped because the promise lives in a chat message, and chat scrolls away.

Every team already has a task tracker. Almost nothing that gets promised in Slack ever reaches it. "I'll send the deck Friday" is a real commitment that exists nowhere but a thread nobody will re-read. Three days later nobody has the deck, and nobody is quite sure whose job it was.

I wanted the layer in between something that catches the work that lives and dies in chat.

What it does

Loose Ends watches channel conversations and catches two things: commitments people make, and questions nobody answered. It stores each as a "loose end," and when a commitment goes overdue or a question goes stale, it nudges the owner privately never a public call-out. Nobody gets shamed in front of the team.

Every nudge is a Block Kit card with real actions: Done · Snooze · Reassign · Escalate to a ticket. There's an App Home dashboard of everything you're on the hook for, sorted by what needs you next. And /looseends ask "what did I commit to this week?" answers from what was actually said in the workspace, with citations back to the real messages.

Crucially, it is conservative. "lol same" and "the build is green" are ignored. So is "I sent the deck yesterday" that's past tense, there's nothing to track. An agent that nags you about nothing gets muted on day one, so false positives are treated as worse than misses. Restraint is the feature.

How we built it

message events → LLM extractor → SQLite → scheduler → Block Kit nudge card
→ actions → App Home dashboard → /looseends ask (RTS) → MCP escalate
  • Bolt for Python on Socket Mode no public URL, no tunnel.
  • LLM extraction behind a swappable wrapper, so the detection brain can be replaced without touching the Slack layer.
  • SQLite via stdlib sqlite3. Zero infra — a hackathon does not need Postgres.
  • APScheduler for the periodic overdue/stale sweep.
  • Our own MCP server — a standalone Python server exposing a create_ticket tool. Escalate is a genuine MCP tool call. Point the connector at real Jira or Linear and the Slack app doesn't change a line.
  • Slack Real-Time Search (assistant.search.context) for grounded, cited answers.

Built solo, in phases, with one rule I stuck to: never cut the spine. Detect → store → nudge → one working button → escalate via MCP → demo. Everything else was negotiable.

Challenges we ran into

The scheduler never actually ran. APScheduler's next_run_time=None doesn't delay a job — it adds it in a paused state. My interval job had therefore never fired once. Every nudge I'd ever seen came from running /looseends check by hand, and I hadn't noticed because that's how I always tested it. The autonomous nudge is the entire premise of the product, and it was quietly switched off. An interval trigger already waits one full interval before its first run, so the parameter I'd added to "prevent firing at boot" was solving a problem that didn't exist — and creating one that mattered.

The gateway silently ate real commitments. The LLM runs behind an OpenAI-compatible gateway that fronts several providers. When it routes badly it ignores the system prompt entirely, and the model answers the Slack message as if it were being chatted to: "Got it! I'll be here whenever you're ready." That isn't JSON, so the loose end was dropped without a trace — the agent looked like it simply didn't care. It was intermittent, so it passed every test I ran, then degraded to a 100% failure rate while I was recording the demo. Requesting response_format: json_object didn't help; the gateway ignores that too. The fix was to stop depending on the gateway's cooperation and carry the instructions in the user turn instead of the system turn message content is the one thing a router can't strip.

Teaching it to shut up. Detecting commitments was easy. Not detecting everything else took most of the tuning.

Accomplishments that we're proud of

  • We didn't just consume an MCP server we built one and open-sourced it. Escalate is a real tool call to our own server, and the connector is swappable for real Jira or Linear without changing a line of the Slack app.
  • Grounded answers, not guesses. /looseends ask uses Slack's Real-Time Search API and cites the actual messages it drew from, with working permalinks.
  • It fails soft, everywhere. MCP server down? Escalate warns instead of crashing. RTS unavailable? /ask falls back to the database and says so in the footer. It never lies about where an answer came from.
  • It knows when to stay quiet. The restraint is deliberate, tested, and on a demo where an agent is expected to show off the thing I'm proudest of.

What we learned

A silent failure is worse than a crash. Both bugs above were invisible: no stack trace, no error, just an agent that appeared not to care. For an accountability tool that's fatal, because the product is trust. "Return a safe default on error" is now something I log loudly rather than swallow.

Test the thing you ship, not the thing you poke. I'd tested the scheduler by triggering it manually which is exactly the code path that wasn't broken.

Restraint has to be designed for. An agent that fires on every message gets muted on day one, and a muted agent is worth nothing.

What's next for Loose Ends

Thread-aware capture, so a promise made three replies deep is caught with its context. Real Jira and Linear connectors behind the same MCP tool interface the seam is already there. Team-level views: who's overloaded, what's been sitting longest, which questions keep going stale. And a weekly digest, so the nudge is something you look forward to rather than something you dismiss.

Built With

  • anthropic
  • apscheduler
  • block-kit
  • claude
  • mcp
  • model-context-protocol
  • python
  • slack
  • slack-bolt
  • slack-real-time-search
  • socket-mode
  • sqlite
Share this project:

Updates