Skip to content

Commit 69d1898

Browse files
committed
fix(linear): teach agent to detect save_issue silent no-ops
DEM-9 (2026-05-27) hit a real failure mode: the agent posted "πŸ”— PR opened" + "βœ… Task completed successfully" and claimed it transitioned the issue to In Review, but the issue stayed in In Progress. Root cause: the DEM team's workflow has no In Review state. When `mcp__linear-server__save_issue` gets a state name that doesn't exist on the team, it silently returns 200 with the issue body unchanged, so it looks like the call succeeded. Update the Linear-channel prompt to tell the agent to: - cache `list_issue_statuses` once per task instead of looping; - check the cached state names before each transition and pick the closest available one if the requested name doesn't exist; - verify the response `state.name` matches what was asked, and NOT claim success when the state didn't actually move. Also forbid embedding `uploads.linear.app/...` URLs in `save_comment` bodies β€” Linear's CDN signed URLs don't render when re-embedded by a bot, producing the broken-image icon you saw on the DEM-9 PR comment. GitHub's image proxy caches the bytes so the same URL works in PRs; agent should link to the PR instead. (cherry picked from commit 2fd73a4)
1 parent 44b3ac3 commit 69d1898

2 files changed

Lines changed: 76 additions & 11 deletions

File tree

β€Žagent/src/prompt_builder.pyβ€Ž

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -162,21 +162,44 @@ def _channel_prompt_addendum(config: TaskConfig) -> str:
162162
f"This task was submitted from Linear issue{issue_ref}. The Linear MCP "
163163
"server is loaded. You MUST perform these updates; they are part of "
164164
"the task contract, not optional:\n\n"
165+
"**State transitions β€” important.** Different Linear teams configure "
166+
"different workflow states. Many teams do NOT have an `In Review` "
167+
"state at all (e.g. only Backlog/Todo/In Progress/Done). When you "
168+
"pass a state name that doesn't exist on the team's workflow, "
169+
"`mcp__linear-server__save_issue` silently no-ops β€” it returns 200 "
170+
"with the issue body unchanged, so it LOOKS like it worked but the "
171+
"state never moves. To avoid this:\n"
172+
" - Call `mcp__linear-server__list_issue_statuses` once at the start "
173+
"of the task and cache the names you got back.\n"
174+
" - Before each transition, check whether the target name is in the "
175+
"cached list. If not, pick the closest available state per the "
176+
"fallbacks below.\n"
177+
" - After each `save_issue`, look at the returned `state.name` field "
178+
"in the response β€” if it's not what you asked for, the transition "
179+
"didn't happen and you should NOT claim it did.\n\n"
180+
"**Comment image rendering β€” important.** Do NOT embed "
181+
"`uploads.linear.app/...` URLs in `save_comment` bodies. Linear's CDN "
182+
"signed URLs work in the original poster's context but render as a "
183+
"broken-image icon when re-embedded in a comment from a different "
184+
"author. If you need to reference an image the user attached, link to "
185+
"it in the GitHub PR (where GitHub's image proxy caches the bytes) or "
186+
"describe it in words. Other URL hosts (imgur, github user-content) "
187+
"are fine to embed.\n\n"
165188
"1. **At start** β€” call `mcp__linear-server__save_comment` with a short "
166189
'"πŸ€– Starting on this issue…" message, then call '
167-
"`mcp__linear-server__save_issue` to transition the issue state. Use "
168-
"`mcp__linear-server__list_issue_statuses` first if you don't already "
169-
"know the state ids; pick the one named `In Progress` (fall back to "
170-
"`Todo` if that state doesn't exist). If the issue is already in "
171-
"`In Progress` or any later state (`In Review`, `Done`), skip the "
172-
"transition. If neither exists, skip β€” the comment alone is enough. "
173-
"Do not invent state names or loop on `list_issue_statuses`.\n"
190+
"`mcp__linear-server__list_issue_statuses` once to get the state map, "
191+
"then call `mcp__linear-server__save_issue` to transition to "
192+
"`In Progress` (fall back to `Todo` if that state doesn't exist). If "
193+
"the issue is already in `In Progress` or any later state (`In Review`, "
194+
"`Done`), skip the transition. If neither exists, skip β€” the comment "
195+
"alone is enough. Do not invent state names.\n"
174196
"2. **When you open the PR** β€” call `mcp__linear-server__save_comment` "
175197
"with the PR URL, then call `mcp__linear-server__save_issue` to "
176-
"transition the issue state to `In Review` (fall back to `In Progress` "
177-
"if that state doesn't exist). If neither exists, skip the state "
178-
"transition β€” the PR comment alone is enough. Do not invent state "
179-
"names or loop on `list_issue_statuses`.\n\n"
198+
"transition to `In Review`. Use the cached state map from step 1. If "
199+
"the team has no `In Review` state, fall back to leaving it at "
200+
"`In Progress` β€” DO NOT silently fail by claiming you transitioned "
201+
"when the response shows the state didn't change. Acknowledge in the "
202+
"PR comment that the team workflow has no In-Review-equivalent.\n\n"
180203
"**Do NOT post a final 'task completed' or 'task failed' comment.** "
181204
"The platform fan-out plane (issue #239) posts a structured "
182205
"βœ…/⚠️/❌ summary on terminal events with cost / turns / duration / "

β€Žagent/tests/test_entrypoint.pyβ€Ž

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,3 +603,45 @@ def test_linear_addendum_inlines_issue_id_and_project_id(self):
603603
# so we surface them directly in the prompt.
604604
assert "issue-uuid-deadbeef" in prompt
605605
assert "project-uuid-cafebabe" in prompt
606+
607+
def test_linear_addendum_warns_save_issue_no_ops_on_unknown_state(self):
608+
# Regression-guard: many Linear teams do NOT have an `In Review`
609+
# state. When the agent passes a state name that doesn't exist,
610+
# save_issue silently no-ops β€” the response shows the unchanged
611+
# state, but the agent claimed success on DEM-9 (2026-05-27).
612+
# The prompt must (a) tell the agent to cache list_issue_statuses,
613+
# (b) check the cached map before each transition, and (c) verify
614+
# the response state.name matches what was asked.
615+
config = TaskConfig(
616+
repo_url="o/r",
617+
task_id="t1",
618+
max_turns=10,
619+
github_token="ghp_test",
620+
aws_region="us-east-1",
621+
channel_source="linear",
622+
channel_metadata={"linear_issue_id": "i"},
623+
)
624+
prompt = _build_system_prompt(config, self._setup(), None, "")
625+
assert "no-op" in prompt or "no op" in prompt
626+
assert "cache" in prompt.lower()
627+
# Must explicitly call out post-transition response verification.
628+
assert "state.name" in prompt or "returned" in prompt.lower()
629+
630+
def test_linear_addendum_warns_against_embedding_uploads_linear_app_in_comments(self):
631+
# Regression-guard: Linear's CDN signed URLs render fine in the
632+
# original poster's context but show a broken-image icon when
633+
# re-embedded by the bot in a comment. Hit on DEM-9 2026-05-27.
634+
config = TaskConfig(
635+
repo_url="o/r",
636+
task_id="t1",
637+
max_turns=10,
638+
github_token="ghp_test",
639+
aws_region="us-east-1",
640+
channel_source="linear",
641+
channel_metadata={"linear_issue_id": "i"},
642+
)
643+
prompt = _build_system_prompt(config, self._setup(), None, "")
644+
assert "uploads.linear.app" in prompt
645+
# The phrasing must be a prohibition for save_comment specifically,
646+
# not just a passing mention β€” make sure we're forbidding the embed.
647+
assert "Do NOT embed" in prompt or "do not embed" in prompt.lower()

0 commit comments

Comments
Β (0)