Conversation
Formalize the design from issue #208 as a canonical draft spec covering: - Render-intent layer boundary between assistant output and Slack delivery - Three lanes: final reply, in-flight progress, durable entities - Plugin renderer registry contract (match/buildIntent/buildFallbackText/ buildActions/buildWorkObject) - SDK-first phasing using the installed chat/@chat-adapter/slack surfaces before adding Slack-specific block abstractions - Accessibility and fallback rules requiring top-level text for every block-bearing message - Failure model, degradation rules, and verification coverage targets The spec sits alongside slack-agent-delivery-spec.md and slack-outbound-contract-spec.md without changing their contracts, and is marked Draft because implementation has not landed yet. Refs: #208 Co-Authored-By: Claude Sonnet 4.5 <devin-ai-integration[bot]@users.noreply.github.com>
…a SKILL.md - Remove work_object_reference intent and the durable-entity lane. - Replace the plugin renderer registry with a guidance model: plugins influence rendering through SKILL.md content, not code or YAML templates. Rendering code stays in core; the intent palette is not plugin-extensible. - Collapse lanes to two: final reply and in-flight progress. - Rework the core render pipeline around one core renderer per intent kind with schema validation and plain_reply degradation on failure. - Drop the work-object observability attribute and durable_entity lane label. Drop work-object coverage from first-party targets. Refs: #208 Co-Authored-By: Claude Sonnet 4.5 <devin-ai-integration[bot]@users.noreply.github.com>
Introduce the closed, core-owned set of Slack render intents defined in specs/slack-rendering-spec.md: - plain_reply (pass-through) - summary_card - alert - comparison_table - result_carousel - progress_plan Each intent is validated by a zod discriminated-union schema. The renderer translates an intent into Slack Block Kit blocks plus a non-empty top-level fallback text derived from the same structured fields, which satisfies the outbound-contract requirement that every block-bearing message carry a non-empty top-level `text`. Wiring these intents into the turn-end path (so the model can emit an intent instead of raw text) is a follow-up change in the same track. Co-Authored-By: Devin <devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: David Cramer <david@sentry.io>
Phase 1 of the Slack rendering engine (issue #208). - Add optional 'reply' tool (Renderer pattern): one tool whose input schema is the SlackRenderIntent discriminated union. Plain text replies keep working unchanged; 'reply' is only called when the model wants a richer intent. - Thread the captured intent from the tool -> AssistantReply -> planSlackReplyPosts -> postSlackApiReplyPosts, rendering blocks + non-empty fallback text at delivery time. - Add GitHub SKILL.md guidance teaching the model when to emit a summary_card for PRs/issues, with exact field recipes. - Update the spec to document the Intent Delivery Mechanism (ToolStrategy, Renderer vs Terminator trade-off, one tool with discriminated union). - Tests: reply-tool schema + capture; planSlackReplyPosts with intent produces blocks+fallback; existing plain text path unaffected. Co-Authored-By: David Cramer <david@sentry.io>
…ce to Linear and Sentry Per PR review: - The rendering engine ships as one coherent feature. Removed Phase 1/ Phase 2 language from the spec; replaced the 'SDK-First Phasing' section with 'Renderer Implementation' that describes what ships and notes that newer Slack block primitives can swap in behind the same intent schema without a contract change. - Strengthened the plugin guidance model to describe both axes plugins influence through SKILL.md: when to use an intent kind for their domain objects and how those objects populate the intent's structured fields. - Added render-intent reference files for Linear (issue, project) and Sentry (issue) so the pattern is proven across three plugins, not just GitHub. Each file documents field recipes, when to prefer alert/carousel, and when not to call reply at all. Co-Authored-By: David Cramer <david@sentry.io>
…im plugin recipes Add a <render-capabilities> section to the Slack system prompt when the native reply tool is registered, so the agent learns the palette and the selection rules from one place instead of duplicating them across plugin SKILL files. Plugin slack-render-intents files now carry only the domain-specific field recipes (GitHub PR/issue, Linear issue/project, Sentry issue) — no more palette preamble, no more 'when not to call reply' boilerplate. Also document the split in specs/slack-rendering-spec.md (section 5 'Guidance Model' + section 8 'Prompt and Model Behavior'): core teaches the capability, plugins teach the recipes. Co-Authored-By: David Cramer <david@sentry.io>
…e <slack-output> section
Agent was emitting GFM markdown tables (pipes render literally in Slack)
when users asked for comparisons. Root causes: (1) the <output-contract>
section carved out 'avoid tables unless explicitly requested', which
gave the model permission to emit pipe-tables; (2) mrkdwn vs GFM rules
were loose ('Slack-friendly markdown') with no syntax enumeration.
Consolidate <output-contract> and <render-capabilities> into a single
authoritative <slack-output> section that:
- Declares two forms: Form A (reply tool call with one palette intent)
and Form B (plain Slack mrkdwn). Forbids any third shape.
- Enumerates allowed mrkdwn syntax explicitly (*bold*, _italic_,
~strike~, <url|label>, etc.) and marks each GFM equivalent as literal.
- Lists forbidden constructs explicitly: markdown tables, # headings,
[label](url), HTML, raw Block Kit.
- Redirects table / comparison / matrix / diff requests to Form A
comparison_table when the reply tool is registered.
Intent rules and mrkdwn rules now live together so the model sees one
coherent Slack surface contract. Extract buildSlackOutputContract as a
top-level helper to keep buildSystemPrompt readable.
Also update specs/slack-rendering-spec.md sections 5 and 8 to reflect
the merged section name.
Co-Authored-By: David Cramer <david@sentry.io>
…als, bump model to gpt-5.4 Previously no eval exercised reply-tool intent selection end-to-end, so a regression like the agent emitting a GFM pipe-table when asked for a comparison would only surface in manual Slack testing. - Capture reply intents in the eval harness by recording AssistantReply.intent on the replyExecutor override and threading it through to EvalResult.replyIntents. - Surface reply_intents on the judge's serialized output schema so rubrics can assert 'the agent called reply with kind=comparison_table' or 'reply_intents is empty for a plain prose answer'. - Add packages/junior-evals/evals/core/slack-render-intents.eval.ts with two scenarios: (1) explicit 'give me a comparison table' must fire the reply tool with comparison_table and must not emit GFM pipe syntax; (2) a plain one-sentence question must not fire the reply tool and must stay in ordinary mrkdwn. - Bump the eval judge model from openai/gpt-5.2 to openai/gpt-5.4 to match the rest of the codebase (chat-config, vision fixtures). Co-Authored-By: David Cramer <david@sentry.io>
…rkdwn formatting guidance Drops the render-intent palette (summary_card, alert, comparison_table, result_carousel, progress_plan, plain_reply), the `reply` tool that selected them, the per-intent renderer, and the plugin-side slack-render-intents.md recipes. The output surface is now plain Slack `mrkdwn` text; the prompt's job is to teach the model which `mrkdwn` syntax Slack actually renders. - `<slack-output>` renamed to `<output surface="slack" ...>` and simplified to an allow-list (`*bold*`, `_italic_`, `~strike~`, inline/fenced code, block quotes, `<url|label>` links, mentions, bullet lists, bold section labels) and a forbid-list (pipe tables, `##` headings, `[label](url)`, `**bold**`, `~~strike~~`, HTML, raw Block Kit JSON). - `slack-rendering-spec.md` rewritten as a short output-contract spec; AGENTS.md and `specs/index.md` updated to match. - `buildRuntimeServices` / `collectResults` / `EvalResult` no longer carry `replyIntents`; `reply_intents` removed from eval output schema. - Plugin SKILL.md files (GitHub, Linear, Sentry) drop references to the deleted `slack-render-intents.md` recipes. - Replaces the render-intent eval with three mrkdwn-hygiene evals (no pipe-tables, Slack-shape emphasis/link syntax, bold section labels instead of markdown headings). Co-Authored-By: Devin <devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: David Cramer <david@sentry.io>
The 'table' keyword has a strong prior in the model's training data that prompt-level rules don't reliably override. Dropping that scenario until we're ready to address it with a deterministic mechanism (outbound post-processor or provider-native response_format). The emphasis/link and bold-section-labels scenarios still catch the failure modes the <output> contract is designed to prevent. Co-Authored-By: Devin <devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: David Cramer <david@sentry.io>
Render final Slack replies through a deterministic normalization pass for the common CommonMark and GFM constructs that Slack does not support. This keeps headings, markdown links, double emphasis, and simple tables from leaking literal syntax into user-visible replies. Keep the Slack surface text-first and remove stale render-intent leftovers so the output contract stays owned by the prompt and output boundary rather than unused block helpers. Refs #208 Co-Authored-By: GPT-5 Codex <noreply@openai.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Keep the deterministic Slack markdown repair layer from corrupting valid markdown links with balanced parentheses in their URLs. Also make markdown table parsing treat Slack link delimiters as part of a cell so linked rows still normalize into a valid fenced table. Add focused regression coverage for both cases so future formatter changes do not reintroduce these rendering failures. Refs #208 Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Teach the Slack normalizer a few more low-risk markdown repairs without broadening it into a full markdown converter. It now strips HTML comments, resolves reference-style markdown links, and keeps the valid Slack-native syntax boundary under explicit regression coverage. This keeps the rendering path narrow and repo-owned while folding in the useful behavior learned from external mrkdwn converter projects. Refs #208 Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Only unwrap raw URLs when formatting encloses a standalone URL token. This avoids corrupting emphasized prose that merely starts with a link. Strip full HTML comments until stable and escape any leftover opener fragments so comment removal cannot reintroduce a literal comment start. Co-Authored-By: Codex GPT-5 <noreply@openai.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7a04e88. Configure here.
| return text | ||
| .replace(/\*\*([^\s*](?:[^\n]*?[^\s*])?)\*\*/g, "*$1*") | ||
| .replace(/~~([^\s~](?:[^\n]*?[^\s~])?)~~/g, "~$1~"); | ||
| } |
There was a problem hiding this comment.
Triple asterisks normalize to forbidden double-bold output
Medium Severity
normalizeCommonMarkEmphasis converts ***text*** into **text** — the exact forbidden CommonMark double-bold the normalizer exists to eliminate. The regex [^\s*] rejects * as the first inner character, so with triple asterisks the match starts one position in, and the surrounding single * characters recombine into **...**. This can arise directly from model output (CommonMark bold+italic) or as a side effect when normalizeMarkdownHeadings wraps already-bold content like ## **Summary** into ***Summary***.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 7a04e88. Configure here.
Keep Slack reply formatting on one shared delivery path instead of splitting finalized replies between the raw API planner and the adapter post path. Escape literal Slack control characters in normalized prose, preserve valid Slack tokens, and upgrade simple single-post markdown tables into native Slack table blocks while keeping section/context fallbacks consistent across live and resumed replies. Document the finalized reply envelope and add contract coverage for the shared raw Slack reply planner so rendering regressions are caught at the boundary where users see them. Refs #208 Co-Authored-By: Codex GPT-5 <noreply@openai.com>


Supersedes #212.
Fix Slack finalized reply rendering by keeping normalized Slack output on one shared delivery path. The normalizer now repairs the CommonMark and GFM constructs that kept leaking into agent answers, escapes literal control characters without corrupting valid Slack tokens, and preserves simple markdown tables long enough for the shared Slack reply planner to upgrade single-post replies into native table blocks.
Before this, finalized replies could still split between the adapter post path and the raw Slack reply planner. That left live replies inconsistent with resumed replies and meant some rendering fixes only applied on one delivery path. This change standardizes finalized Slack replies on the shared raw Web API planner whenever we have a concrete Slack thread target, while keeping plain-text fallbacks and footer/context behavior centralized.
The footer-only helper is now separated from reply block building, table parsing stays isolated, and the Slack rendering, outbound, and delivery specs now describe the shipped contract instead of the abandoned render-intent direction.
Refs #208