Skip to content

Commit f7c8730

Browse files
committed
feat(agent): rewrite Linear prompt guidance for the no-MCP deterministic model (ADR-016)
The agent has no Linear MCP and no Linear write access, so strip all mcp__linear-server__* guidance from the channel addendum + context-probe hint: - prompt_builder._channel_prompt_addendum: collapse the four per-workflow MCP choreographies (progress comments, list_issue_statuses caching, save_issue state transitions, on-demand get_issue/get_attachment/ extract_images/list_documents/list_comments) into one deterministic block — 'context is already here (pre-hydrated), status is automatic (platform posts reactions/state/comments/summary), just do the code work.' Drops _linear_context_discovery_section entirely. - linear-issue-context-probe.renderIssueContextHint: keep the presence signal (paperclip attachments / project docs exist) but remove the dead fetch-tool references; tell the agent these live in Linear and aren't attached, so note the gap in the PR rather than guessing. Deterministic coverage already in place: linear_reactions.py posts 👀/✅/❌ + Backlog→In Progress→In Review; the fan-out plane posts the terminal summary; P4.5 posts the start + PR-opened comments. Tests updated to the no-MCP contract.
1 parent 6451740 commit f7c8730

5 files changed

Lines changed: 104 additions & 307 deletions

File tree

agent/src/prompt_builder.py

Lines changed: 34 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -273,180 +273,55 @@ def _render_memory_context(hydrated_context: HydratedContext | None) -> str:
273273
def _channel_prompt_addendum(config: TaskConfig) -> str:
274274
"""Return channel-specific prompt guidance, or empty string.
275275
276-
For Linear-origin tasks, instruct the agent to post progress comments and
277-
transition state using the already-loaded Linear MCP tools. The tool names
278-
are stated explicitly so the agent doesn't grope for them.
276+
Linear-origin tasks (ADR-016 "Linear is fully deterministic"): the agent has
277+
NO Linear MCP and NO Linear write access. All Linear I/O is handled by the
278+
platform, not the agent:
279+
* inbound context — the issue title/description, recent human comments, and
280+
attachments are ALREADY pre-hydrated into the task description +
281+
``attachments`` at admission time (linear-webhook-processor +
282+
linear-attachments.ts + linear-feedback.fetchRecentComments). There is
283+
nothing to fetch at runtime.
284+
* outbound status — 👀/✅/❌ reactions and Backlog→In Progress→In Review
285+
state transitions are posted deterministically by ``linear_reactions.py``;
286+
the "🤖 Starting" and PR-opened comments are posted at the Lambda tier;
287+
the terminal ✅/⚠️/❌ summary (cost/turns/PR link) is posted by the
288+
fan-out plane. So the addendum's whole job now is to tell the agent to
289+
do the code work and NOT attempt any Linear calls.
279290
280291
Jira-origin tasks intentionally get NO addendum: Atlassian's Remote MCP
281292
requires an interactive OAuth flow a headless agent can't complete, so the
282-
MCP tools never load. Instructing the agent to use them just wastes turns.
283-
Jira progress comments are posted out-of-band by ``jira_reactions`` (a REST
284-
shim wired into the pipeline), not by the agent.
293+
MCP tools never load. Jira progress comments are posted out-of-band by
294+
``jira_reactions`` (a REST shim wired into the pipeline), not by the agent.
285295
"""
286296
if config.channel_source != "linear":
287297
return ""
288298
# #247 UX.16: a synthetic orchestration integration node has NO real Linear
289299
# sub-issue — `linear_issue_id` is intentionally omitted from its
290300
# channel_metadata (see orchestration-release.ts). Without a target issue
291-
# the agent would grope via the MCP and post its "Starting"/"PR opened"
292-
# comments onto the PARENT epic, cluttering the maturing panel (which
293-
# already shows the integration row + combined PR + preview). Skip the
294-
# progress addendum entirely for these nodes — the panel is the surface.
301+
# there is nothing issue-specific to say; the parent panel is the surface.
295302
if not config.channel_metadata.get("linear_issue_id"):
296303
return ""
297304
issue_identifier = config.channel_metadata.get("linear_issue_identifier") or ""
298305
issue_ref = f" (`{issue_identifier}`)" if issue_identifier else ""
299-
issue_id = config.channel_metadata.get("linear_issue_id") or ""
300-
project_id = config.channel_metadata.get("linear_project_id") or ""
301-
302-
# iteration-UX: a comment-iteration (pr-iteration-v1, triggered by an
303-
# @bgagent comment) is surfaced by the platform's single maturing threaded
304-
# reply (👀 On it → 🔄 Working → ✅/💬 + cost). The agent's own top-level
305-
# "🤖 Starting" / "🔗 PR opened" comments would just re-clutter the issue
306-
# with the comments we removed (ABCA-430). So for iterations, suppress the
307-
# progress-comment instructions and post ONLY the context-discovery half +
308-
# the state-transition guidance. (new_task keeps its headline comments — they
309-
# ARE the issue's first signal.)
310-
workflow_id = (config.resolved_workflow or {}).get("id", "")
311-
312-
# #299 agent-native planning: a coding/decompose-v1 task PLANS, it doesn't
313-
# change the repo. The platform posts the plan proposal (🗂️) from the agent's
314-
# artifact and owns the whole approval conversation, so the agent must NOT do
315-
# the coding-task Linear choreography: no "🤖 Starting" comment, no state
316-
# transition (a planning run shouldn't move the issue to In Progress), no PR
317-
# steps, no "task completed". Those cluttered the plan thread (live-caught on
318-
# ABCA-510). MCP stays loaded for on-demand context discovery only.
319-
if workflow_id == "coding/decompose-v1":
320-
return (
321-
"\n\n## Linear issue (planning only)\n\n"
322-
f"This is a DECOMPOSITION-PLANNING task on Linear issue{issue_ref}. You are "
323-
"planning how to break the work down; you are NOT changing the repo. The "
324-
"platform posts your plan and runs the approval conversation. So do NOT "
325-
"post any Linear comments (no 'Starting', no 'task completed'), and do NOT "
326-
"transition the issue's state — just emit the plan JSON as your final "
327-
"message per your workflow instructions. The Linear MCP is loaded ONLY for "
328-
"on-demand context discovery below (read attachments / comments / documents "
329-
"if you need them to plan).\n" + _linear_context_discovery_section(issue_id, project_id)
330-
)
331-
332-
is_comment_iteration = workflow_id == "coding/pr-iteration-v1"
333-
if is_comment_iteration:
334-
return (
335-
"\n\n## Linear issue progress (iteration)\n\n"
336-
f"This is a follow-up iteration on Linear issue{issue_ref}, triggered "
337-
"by a comment. The platform posts a single threaded status reply under "
338-
"that comment (it shows progress + cost), so **do NOT post your own "
339-
"'Starting' / 'PR opened' / 'task completed' Linear comments** — they "
340-
"duplicate the platform reply and clutter the issue. The Linear MCP is "
341-
"still loaded; use it ONLY for the on-demand context discovery below "
342-
"(fetching attachments / comments / documents when you need them). Do "
343-
"the code work and let the platform narrate it.\n"
344-
+ _linear_context_discovery_section(issue_id, project_id)
345-
)
346306

347307
return (
348-
"\n\n## Linear issue progress updates (REQUIRED)\n\n"
349-
f"This task was submitted from Linear issue{issue_ref}. The Linear MCP "
350-
"server is loaded. You MUST perform these updates; they are part of "
351-
"the task contract, not optional:\n\n"
352-
"**State transitions — important.** Different Linear teams configure "
353-
"different workflow states. Many teams do NOT have an `In Review` "
354-
"state at all (e.g. only Backlog/Todo/In Progress/Done). When you "
355-
"pass a state name that doesn't exist on the team's workflow, "
356-
"`mcp__linear-server__save_issue` silently no-ops — it returns 200 "
357-
"with the issue body unchanged, so it LOOKS like it worked but the "
358-
"state never moves. To avoid this:\n"
359-
" - Call `mcp__linear-server__list_issue_statuses` once at the start "
360-
"of the task and cache the names you got back.\n"
361-
" - Before each transition, check whether the target name is in the "
362-
"cached list. If not, pick the closest available state per the "
363-
"fallbacks below.\n"
364-
" - After each `save_issue`, look at the returned `state.name` field "
365-
"in the response — if it's not what you asked for, the transition "
366-
"didn't happen and you should NOT claim it did.\n\n"
367-
"**Comment image rendering — important.** Do NOT embed "
368-
"`uploads.linear.app/...` URLs in `save_comment` bodies. Linear's CDN "
369-
"signed URLs work in the original poster's context but render as a "
370-
"broken-image icon when re-embedded in a comment from a different "
371-
"author. If you need to reference an image the user attached, link to "
372-
"it in the GitHub PR (where GitHub's image proxy caches the bytes) or "
373-
"describe it in words. Other URL hosts (imgur, github user-content) "
374-
"are fine to embed.\n\n"
375-
"1. **At start** — call `mcp__linear-server__save_comment` with a short "
376-
'"🤖 Starting on this issue…" message, then call '
377-
"`mcp__linear-server__list_issue_statuses` once to get the state map, "
378-
"then call `mcp__linear-server__save_issue` to transition to "
379-
"`In Progress` (fall back to `Todo` if that state doesn't exist). If "
380-
"the issue is already in `In Progress` or any later state (`In Review`, "
381-
"`Done`), skip the transition. If neither exists, skip — the comment "
382-
"alone is enough. Do not invent state names.\n"
383-
"2. **When you open the PR** — call `mcp__linear-server__save_comment` "
384-
"with the PR URL, then call `mcp__linear-server__save_issue` to "
385-
"transition to `In Review`. Use the cached state map from step 1. If "
386-
"the team has no `In Review` state, fall back to leaving it at "
387-
"`In Progress` — DO NOT silently fail by claiming you transitioned "
388-
"when the response shows the state didn't change. Acknowledge in the "
389-
"PR comment that the team workflow has no In-Review-equivalent.\n\n"
390-
"**Do NOT post a final 'task completed' or 'task failed' comment.** "
391-
"The platform fan-out plane (issue #239) posts a structured "
392-
"✅/⚠️/❌ summary on terminal events with cost / turns / duration / "
393-
"PR-link metrics that you don't have visibility into. A redundant "
394-
"agent-side completion comment would just stack two near-identical "
395-
"comments on the issue.\n\n"
396-
"Keep the start + PR-opened comments concise. Do not mirror the full "
397-
"agent transcript back to Linear.\n\n"
398-
+ _linear_context_discovery_section(issue_id, project_id)
399-
)
400-
401-
402-
def _linear_context_discovery_section(issue_id: str, project_id: str) -> str:
403-
"""The on-demand Linear MCP context-discovery guidance.
404-
405-
Shared by the new-task progress addendum and the iteration addendum (where
406-
the start/PR-opened comments are suppressed but context discovery still
407-
applies). Pure string-builder.
408-
"""
409-
return (
410-
"## Linear context discovery (on demand)\n\n"
411-
"The same Linear MCP exposes tools for fetching extra context on the "
412-
"issue when you need it. Use them sparingly — only when the task "
413-
"description references material you don't have, when the description "
414-
"is ambiguous and project-level context would clarify, or when a "
415-
"decision point benefits from a fresh look at the issue thread. Do "
416-
"NOT call these on every task; the issue title + description are "
417-
"usually sufficient.\n\n"
418-
f"- **Issue + paperclip attachments.** Call `mcp__linear-server__get_issue` "
419-
f'with `id: "{issue_id}"` to fetch the full issue, including its '
420-
"`attachments` connection (paperclip-icon files like PDFs, logs, "
421-
"spec docs that aren't embedded as markdown images). Read the "
422-
"attachment titles first; for each one that looks relevant, call "
423-
"`mcp__linear-server__get_attachment` with that attachment id. Skip "
424-
"ones that look unrelated (e.g. screenshots from prior debugging "
425-
"sessions).\n"
426-
"- **Embedded images.** Description and comment images that look "
427-
"like `![alt](https://uploads.linear.app/…)` may have stale signed "
428-
"URLs by the time you run. If you need to actually look at one, call "
429-
"`mcp__linear-server__extract_images` to get fresh signed URLs, then "
430-
"use the built-in `WebFetch` tool to download. (The screened "
431-
"description-image path runs at task-creation time and is separate "
432-
"from this — you don't need to re-screen.)\n"
433-
"- **Project documents.** When the issue belongs to a project and "
434-
"the task is ambiguous enough that project-level context (specs, "
435-
"design docs, RFCs) would help, call "
436-
f"`mcp__linear-server__list_documents` filtered to "
437-
f'`projectId: "{project_id}"` (skip if the issue has no project). '
438-
"Read the titles. For documents that clearly relate to your task, "
439-
"call `mcp__linear-server__get_document` to read the body. Don't "
440-
"fetch every document.\n"
441-
"- **Comments posted after task start.** Comments left while you're "
442-
"running (e.g. clarifications, approve/deny signals from the "
443-
"requester) are not in your task description. Before opening the PR, "
444-
f"and again before merging if asked, call `mcp__linear-server__list_comments` "
445-
f'with `issueId: "{issue_id}"` and look for new comments since '
446-
"task start. Respect any clear approve / deny / block / hold signals "
447-
"from the original requester (the issue creator or the person who "
448-
"applied the trigger label) — if they say stop, stop and post a "
449-
"comment explaining why."
308+
"\n\n## Linear issue\n\n"
309+
f"This task was submitted from Linear issue{issue_ref}. The platform "
310+
"manages ALL Linear interaction for you — you have no Linear tools and "
311+
"must not try to call any:\n\n"
312+
"- **Context is already here.** The issue title, description, recent "
313+
"human comments, and any attachments the reporter added have been "
314+
"pre-fetched and included in your task description + attachments. There "
315+
"is nothing to fetch from Linear — work from what you've been given.\n"
316+
"- **Status is automatic.** The platform posts the issue reactions "
317+
"(👀 on start, ✅/❌ on finish), moves the issue through its workflow "
318+
"states (In Progress → In Review), posts the start + PR-opened comments, "
319+
"and posts the final ✅/⚠️/❌ summary with cost/PR-link metrics. Do NOT "
320+
"post Linear comments or change the issue state yourself — you'd only "
321+
"duplicate the platform's messages.\n\n"
322+
"Just do the code work: make the change, open the PR, and let the "
323+
"platform narrate it. Reference issues/PRs in your GitHub PR description "
324+
"as usual.\n"
450325
)
451326

452327

agent/tests/test_entrypoint.py

Lines changed: 15 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -527,8 +527,7 @@ def test_no_addendum_when_channel_is_blank(self):
527527
aws_region="us-east-1",
528528
)
529529
prompt = _build_system_prompt(config, self._setup(), None, "")
530-
assert "Linear issue progress updates" not in prompt
531-
assert "Linear context discovery" not in prompt
530+
assert "## Linear issue" not in prompt
532531

533532
def test_no_addendum_for_slack_channel(self):
534533
config = TaskConfig(
@@ -540,8 +539,7 @@ def test_no_addendum_for_slack_channel(self):
540539
channel_source="slack",
541540
)
542541
prompt = _build_system_prompt(config, self._setup(), None, "")
543-
assert "Linear issue progress updates" not in prompt
544-
assert "Linear context discovery" not in prompt
542+
assert "## Linear issue" not in prompt
545543

546544
def test_addendum_present_for_linear_channel(self):
547545
config = TaskConfig(
@@ -558,13 +556,13 @@ def test_addendum_present_for_linear_channel(self):
558556
},
559557
)
560558
prompt = _build_system_prompt(config, self._setup(), None, "")
561-
assert "Linear issue progress updates" in prompt
562-
assert "Linear context discovery" in prompt
559+
assert "## Linear issue" in prompt
563560
assert "ABC-42" in prompt
564561

565-
def test_linear_addendum_names_attachment_tools(self):
566-
# The agent must know the exact MCP tool names — vague references
567-
# would cause it to grope. Lock these in so a rename triggers the test.
562+
def test_linear_addendum_references_no_mcp_tools(self):
563+
# ADR-016: Linear is fully deterministic — the agent has no Linear MCP.
564+
# The addendum must NOT name any mcp__linear-server__* tool (a leftover
565+
# reference would send the agent groping for a tool that doesn't exist).
568566
config = TaskConfig(
569567
repo_url="o/r",
570568
task_id="t1",
@@ -575,62 +573,11 @@ def test_linear_addendum_names_attachment_tools(self):
575573
channel_metadata={"linear_issue_id": "issue-uuid-1"},
576574
)
577575
prompt = _build_system_prompt(config, self._setup(), None, "")
578-
for tool in (
579-
"mcp__linear-server__get_issue",
580-
"mcp__linear-server__get_attachment",
581-
"mcp__linear-server__extract_images",
582-
"mcp__linear-server__list_documents",
583-
"mcp__linear-server__get_document",
584-
"mcp__linear-server__list_comments",
585-
):
586-
assert tool in prompt, f"expected {tool} to be named in the Linear addendum"
587-
588-
def test_linear_addendum_inlines_issue_id_and_project_id(self):
589-
config = TaskConfig(
590-
repo_url="o/r",
591-
task_id="t1",
592-
max_turns=10,
593-
github_token="ghp_test",
594-
aws_region="us-east-1",
595-
channel_source="linear",
596-
channel_metadata={
597-
"linear_issue_id": "issue-uuid-deadbeef",
598-
"linear_project_id": "project-uuid-cafebabe",
599-
},
600-
)
601-
prompt = _build_system_prompt(config, self._setup(), None, "")
602-
# The agent shouldn't have to guess the ids — they're in the metadata,
603-
# so we surface them directly in the prompt.
604-
assert "issue-uuid-deadbeef" in prompt
605-
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.
576+
assert "mcp__linear-server" not in prompt
577+
578+
def test_linear_addendum_states_context_prehydrated_and_status_automatic(self):
579+
# The agent must be told (a) inbound context is already provided (nothing
580+
# to fetch) and (b) not to post Linear comments or change state.
634581
config = TaskConfig(
635582
repo_url="o/r",
636583
task_id="t1",
@@ -641,7 +588,6 @@ def test_linear_addendum_warns_against_embedding_uploads_linear_app_in_comments(
641588
channel_metadata={"linear_issue_id": "i"},
642589
)
643590
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()
591+
assert "Context is already here" in prompt
592+
assert "Status is automatic" in prompt
593+
assert "Do NOT post Linear comments" in prompt

0 commit comments

Comments
 (0)