Skip to content

feat: record_on_issue — let Slack chat runs comment on in-scope issues (0.12.0) - #20

Merged
0xCVH merged 2 commits into
mainfrom
feat/record-on-issue
Sep 30, 2026
Merged

0xCVH merged 2 commits into
mainfrom
feat/record-on-issue

Conversation

@0xCVH

@0xCVH 0xCVH commented Sep 30, 2026

Copy link
Copy Markdown
Owner

Implements docs/2026-09-30-slack-record-on-issue-spec.md (paperclip repo), amended after an adversarial review against server 2026.930.0-canary.3 and the prod host (2026.916.0-canary.0).

Problem

A plugin-started Slack chat run has no issueId/taskId in its run context (plugin-host-services.js sendMessage), and observeCrossIssueInfluence throws cross_issue_influence_run_context_required before it compares source and target. So the Slack agent can't write to any issue, including the thread's own. Seen on POL-3267 (2026-09-30) and POL-3099 (2026-09-29, where Raúl's "the fix implemented was PR #53" had to be relayed through a child issue, POL-3206).

Change

New agent tool record_on_issue({ issue, body, wakeAssignee? }). It writes through ctx.issues.createComment, which doesn't run the cross-issue guard, so the plugin enforces the containment itself:

# Rule Refusal code
1 Run company matches the bound config company_mismatch
2 Run has a live chat-run binding for the same agent (written from sendMessage's runId; live until 30 min after the turn settles, 24 h cap) not_a_slack_chat_run
3 Issue resolves in the company issue_not_found
– Issue isn't under a trust preset (the host would quarantine an agent comment there; this path can't tag it) low_trust_target
4 Issue is the thread's own (plugin thread→issue link, or a paperclipBaseUrl issue link in a root this bot posted, read from text and Block Kit links), or a human named it in the thread or the triggering message issue_not_in_thread_scope
6 Same body on the same issue in the same run returns the original comment (checked before the cap) ok, duplicate: true
5 At most 20 writes per run, serialized per run run_write_cap_exceeded

Changes from the spec, from the review:

  • Forged roots: any agent can post a top-level bot message with an issue link through slack_post_message, which would fake rule 4a.2. Top-level posts with agent-written text (slack_post_message, ask_human, top-level DM replies) are now recorded and never count as issue-linking. Roots older than that 90-day record window don't count either. paperclip-escalate posts directly with chat.postMessage, so real escalations still work.
  • issues.read capability: added and required. The host's createComment hands the raw id to addComment, so it needs the UUID, and the plugin also needs the assignee. Operator re-approval is required on upgrade.
  • Reverse lookup: a new thread-issue:<channel>:<ts> key, written with the issue-created notification, replaces scanning the issue-thread index.
  • Blocks: the gateway now exposes fromAnyBot and blockLinks on thread messages, and getPermalink (chat.getPermalink, no extra scope).

What is written: an agent-attributed comment (authorAgentId) with a footer the agent can't suppress: Relayed from Slack · <thread permalink> · run <runId>. Then an activity-log entry (metadata.action: slack.record_on_issue, with the scope rule), metrics tagged with code only, and a best-effort assignee wake (idempotency key per run and issue, never the caller).

Rollout (not part of this PR)

  1. Update the Paperclip Slack agent's instructions to use record_on_issue instead of POST /api/issues/:id/comments in chat runs. The file is on the host: ~paperclip/.paperclip/instances/default/companies/79544d4d-2f8a-459b-a316-cef2d510566e/agents/e2d936fd-d104-481e-bb34-a3dacd31367f/instructions/AGENTS.md.
  2. Install 0.12.0, approve issues.read, restart paperclip.service.
  3. Check that the plugin's paperclipBaseUrl is https://scc.polygon.org, since rule 4a.2 matches only that origin.
  4. Verify in a live escalation thread: record works, the footer and activity row are present, the assignee is woken; an unrelated issue gets issue_not_in_thread_scope. This also confirms the tool call's runId equals the sendMessage runId.

Testing

  • 29 new tests in tests/record-on-issue.test.ts, including a regression built from the POL-3099 thread. Mutation checks: removing the agent-post exclusion, the agent-id check, the bot-text filter, the cap, or the age limit each fails a test.
  • Updated pinned tests: manifest capabilities and tools, worker tool registration, and the gateway thread mapping (plus a new block-links test).
  • Full suite: 579/579; both tsconfigs clean; build clean.

🤖 Generated with Claude Code

0xCVH and others added 2 commits September 30, 2026 12:40
…issues

A plugin-started chat run has no issue in its host run context, so the
host's cross-issue guard refuses every comment it makes, including on
the thread's own issue. record_on_issue writes through the plugin's
createComment path instead, which skips that guard, and enforces the
containment in the plugin:

- only runs this plugin bound to a Slack conversation (run -> thread
  binding written from sendMessage's runId, agent id checked, expires
  30 min after the turn settles, 24 h hard cap)
- target must be the thread's own issue (plugin thread->issue link, or
  an issue link on paperclipBaseUrl in a root this bot posted, read
  from text and Block Kit links) or an issue a human named in the
  thread; bot and foreign-bot text never widen scope
- roots whose words an agent chose (slack_post_message, ask_human,
  top-level DM replies) are recorded and never count as issue-linking;
  roots older than the 90-day record window don't either
- 20 writes per run (mirrors CROSS_ISSUE_INFLUENCE_LIMIT), serialized
  per run, dedupe checked before the cap
- refuses issues under a trust preset (the host would quarantine an
  agent comment there; this path can't tag it)
- UUID-resolved target, agent-attributed comment, fixed footer with
  thread permalink and run id, activity log + code-tagged metrics,
  best-effort idempotent assignee wake, never the caller

Adds the issues.read capability (identifier -> UUID and assignee),
which needs operator re-approval on upgrade. Bumps to 0.12.0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…6.16.0, fast-uri 3.1.8

The high-severity undici advisory (WebSocket permessage-deflate DoS) is in
the Socket Mode transport (@slack/bolt -> @slack/socket-mode -> undici) and
fails CI's npm audit step. Lockfile-only, in-range bumps.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@0xCVH
0xCVH merged commit 867cdf2 into main Sep 30, 2026
7 checks passed
@0xCVH
0xCVH deleted the feat/record-on-issue branch September 30, 2026 10:45
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.

1 participant