fix(linear-slack): deliver Slack replies + guard the writeback-scope footgun

Merged
KH
5mo ago
+416-1

What happened

linear-slack was engaged in Slack and silently dropped its reply — nothing appeared in the channel, yet the run logged handler.ok. Traced live via the Daytona CLI into the running sandbox:

  • The trigger fired, the harness ran (~3m17s) and produced a reply.
  • slackClient().post() doesn't call the Slack API — it writes a draft into the VFS mount and polls for a writeback receipt. The reply draft was recovered, orphaned and undelivered, at /slack/channels/C0B9287EP6Y/messages/messages <uuid>.json.

Root cause: the labelled-mirror footgun

  • The persona used scope-less slack: {}. The only Slack mount was the trigger's read-only mirror of the display-labelled channel path /slack/channels/{id}__{name}/messages.
  • But the writeback path is canonical bare-id /slack/channels/{id}/messages. They never coincide, so the draft landed on unmounted disk and the writeback worker never flushed it.
  • vfs-client.writeJsonFile returns receipt: undefined on timeout and slackClient.post returns { ts: '' } without throwing → the runtime logged handler.ok with nothing delivered.

(Bonus finding: the "AR-83" issue the agent claimed to create was also fake — it wrote local JSON into the read-only Linear mirror, not a real Linear mutation. Out of scope for this PR.)

Changes

  1. persona.ts — fix delivery. Scope slack to /slack/channels/** so the writeback path is actually mounted. Same fix already shipped in review/vendor-monitor/hn-monitor/repo-hygiene; linear-slack was the last holdout.
  2. agent.ts — make failures loud. postReply now treats an empty receipt ts as a delivery failure and throws, so a dropped reply surfaces as handler.error instead of a false handler.ok.
  3. tests/persona-integration-scopes.test.mjs — tighten the guard. The existing guard treated trigger OR scope as sufficient, which false-negatived this case (linear-slack has a slack trigger). New sub-rule: a write to a display-labelled provider (WRITEBACK_NEEDS_SCOPE = {slack}) requires a scope even when a trigger exists. github/linear are unaffected — their trigger and writeback paths share one bare-id form. Proven red→green against the original config.

This PR also adds the linear-slack agent itself (it was deployed but never committed). It ships only agent.ts + persona.ts; README/graphics to follow if we want it in the showcase set.

Preventing this for users

Repo-local guards/docs only protect this repo. Framework-level prevention is tracked in AgentWorkforce/cloud#2029 — chiefly: make slackClient write methods reject on a missing receipt (so every future occurrence is a visible error, not a silent no-op), plus auto-mounting writeback paths and a persona-kit compile-time check. The writing-agent-personas skill §1 is updated with the labelled-mirror sub-trap (separate PR in AgentWorkforce/skills).

Test

npm run typecheck clean; npm test 54/54, including the tightened guard proven red on the broken config and green on the fix.

🤖 Generated with Claude Code


Summary by cubic

Fixes undelivered Slack replies in linear-slack by mounting the writeback path and failing fast on missing delivery receipts. Also adds the agent to the repo and tightens the scope guard to prevent future silent Slack writebacks.

  • Bug Fixes

    • Scoped Slack to /slack/channels/** so writebacks use a mounted path and replies deliver.
    • postReply now throws when Slack returns an empty ts, surfacing dropped replies as handler.error.
    • Tightened integration-scope guard: Slack writes require a non-empty scope even with a trigger; prevents drafts landing on unmounted disk. GitHub/Linear unaffected.
  • New Features

    • Added the linear-slack agent and persona (agent.ts, persona.ts).