fix(linear-slack): deliver Slack replies + guard the writeback-scope footgun
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.writeJsonFilereturnsreceipt: undefinedon timeout andslackClient.postreturns{ ts: '' }without throwing → the runtime loggedhandler.okwith 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
persona.ts— fix delivery. Scope slack to/slack/channels/**so the writeback path is actually mounted. Same fix already shipped inreview/vendor-monitor/hn-monitor/repo-hygiene;linear-slackwas the last holdout.agent.ts— make failures loud.postReplynow treats an empty receipttsas a delivery failure and throws, so a dropped reply surfaces ashandler.errorinstead of a falsehandler.ok.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 ascopeeven 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. postReplynow throws when Slack returns an emptyts, surfacing dropped replies ashandler.error.- Tightened integration-scope guard: Slack writes require a non-empty
scopeeven with a trigger; prevents drafts landing on unmounted disk. GitHub/Linear unaffected.
- Scoped Slack to
-
New Features
- Added the
linear-slackagent and persona (agent.ts,persona.ts).
- Added the