Skip to content

feat(workflows): request a change to a code-managed email template through PostHog Code - #119032

Closed
Silthus wants to merge 47 commits into
PostHog:masterfrom
Silthus:spec/template-request-change
Closed

Silthus wants to merge 47 commits into
PostHog:masterfrom
Silthus:spec/template-request-change

Conversation

@Silthus

@Silthus Silthus commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Important

Stacks on #118980 (spec/template-agent-edit), which stacks on #118924. GitHub can't base a PR on a fork branch, so this targets master. Review starts after stack A's tip, 0960e3de116: git diff 0960e3de116...spec/template-request-change.

Problem

A person who can't reach a code-managed email template's repository has no way to get the template changed.
The template page is read-only (#118924), and #118980 only hands the person a prompt for their own coding agent.
A marketer without a coding agent, PostHog AI in the sidebar, and the Slack bot can't start the change.

Caution

🔴 Merge risk: high · one-way door (merge-risk rubric)

  • A new route and MCP tool start a billable PostHog Code task with write access to the customer's GitHub repository. The task's credits and the pull request it opens outlive a revert.
  • Limits: everything sits behind workflows-transactional-email-api (off). Project secret API keys and calls from inside PostHog Code are refused, callers need hog_flow:read and task:write plus editor access to workflows, and the task opens a pull request that a person merges. PostHog never saves, uploads or syncs the template.

Changes

On a code-managed template, "Edit with your agent" gains Run with PostHog Code. Only flagged uploads make a template code-managed, so nobody sees it today.

  • Request a change. POST /api/projects/{id}/messaging_templates/{id}/request_change/ with {instruction} starts a PostHog Code task. The task edits the template's file in its GitHub repository and opens a pull request. The response is 201 {task_id, task_url, repository}.
  • The task prompt is the person's sentence, the same "About this file" block the copied prompt uses, and the rule to change only this file and leave the upload to after the merge.
  • Who may call it: sessions, personal API keys and OAuth tokens with hog_flow:read and task:write, and editor access to workflows. Project secret API keys get 403. A call from inside a PostHog Code sandbox gets posthog_code_not_available, so PostHog AI and the Slack bot hand the person the template link instead.
  • Errors use the feat(messaging): upload, send and lock code-managed email templates #118924 envelope (code, detail, why, fix, docs_url): template_not_managed_by_code, template_repository_missing, template_repository_unsupported, github_repository_not_connected, posthog_code_not_available, instruction_missing, posthog_code_run_failed.
  • The button is shown only when PostHog Code is available to the organization. It says why it can't run: no workflow editor access, no repository, GitHub not connected, the change not described, or longer than 2,000 characters. It ignores a second click while the request runs. A refusal shows detail and fix in the popover, or as a toast when the popover was closed. Success shows a toast and a task link in the notice.
  • Agents never update or patch a code-managed template, and never rebuild and upload it from PostHog. With the repository in their workspace they edit the file. Inside PostHog Code they share the template link. Any other agent calls the new MCP tool workflows-request-email-template-change. The designing-email-templates skill, the tool texts, the 403 immutable fix and the sidebar page context say this in the same order.
  • Events: message template change run requested and … run failed (button), message template change requested and … change refused (server, with surface). No instruction text, path or repository in properties.
  • Mechanical: regenerated OpenAPI types and MCP tool definitions; tach.toml lets messaging use the tasks facade.

Captures from the real app built from this branch at 0c3c8b87b51, on a scratch project with invented content (Hedgebox, example.com):

Change described, Run with PostHog Code next to Copy prompt (1440px) Same at 560px, about the scene width with the side panel open
The popover with a described change and the Run with PostHog Code button The same popover at 560px
Refused: the route's detail and fix in the popover (560px) Started: toast and task link in the notice (1440px)
A repository the GitHub integration can't reach, with the server's detail and fix The page after PostHog Code started, with the toast and the notice's Open task link

Reading order

Ticket Commits What
Silthus/posthog#513 804a0b87d6c Route, PostHog Code task, error codes, MCP tool, 403 immutable fix text
Silthus/posthog#514 60fb98f06a5 "Run with PostHog Code" in the popover
Silthus/posthog#515 fbbbe6ac4f1, df551ee9fb6 Eval harness; skill, page context, tool text, evals
Merges 495bca44dec, 85da5752479 Stack A into this branch
Review round 1 6a8e0376690, e00a29226a4, 29a52f31796 hog_flow:read scope, agent texts point at change_prompt_context, .. paths unknown, refusal event; button access and length reasons, toast when closed; eval reads the page's real context
Review round 2 fc5dbc9aa98, 0c3c8b87b51 Refusal event on every refusal, one wording for when an agent edits the file, upload rule in the upload tool; skill chip not dismissible, code-point length

How did you test this code?

Live run, Django built from this branch on a scratch database, with the flag forced on and create_and_run_task wrapped to pass start_workflow=False. That creates a real Task row and starts no sandbox. The Desktop access, credits and GitHub repository checks ran for real. At 0c3c8b87b51:

  • A personal API key started a task. The task got repository example/app, create_pr, read-only PostHog scopes, and the full description. The template's hash and its workflow's version stayed unchanged.
  • A project secret key with task:write, a key without task:write, and a call with the sandbox header got 403. A template made in PostHog got template_not_managed_by_code.
  • In the real app (Chromium, 1440 and 560 px): the button starts one task on a double click, a repository the GitHub integration can't reach shows detail and fix in the popover, and success shows the toast and the notice link.

Shadow diff, base 0960e3de116 against HEAD, the same requests on identical scratch databases. Upload, list, GUI edits and release answer byte for byte the same. The only differences are the new route, the new 403 immutable fix text, and a source path with .. now being unknown in the prompt.

Test rationale:

  • test_template_change_request.py (APIBaseTest, templates made by the real upload route, the tasks facade mocked at its function boundary as the spec asks): the exact task arguments and description, every refusal code, the server events, project secret keys and tokens without hog_flow:read or task:write refused, and the template and workflow unchanged.
  • test_code_managed_template_lock.py: the exact 403 immutable fix, including a path that could carry an instruction and one that leaves the repository.
  • test_template_change_prompt.py: a .. path is unknown.
  • messageTemplateLogic.test.ts (API mocked at HTTP): the exact request body, one request on a double click, each disabled reason, availability, success, refusal in the popover and as a toast.
  • templateAgentContext.test.ts: the page context, a closed chip on one template keeping every other page's context (through the context store), and the eval's sidebar fixture rendered from the real builders.
  • Eval harness and scorer tests, including an upload by key counted as touching the template.

Local gate on HEAD: messaging backend, eval harness, MCP tool definitions and repo invariants 1,615 passed; mypy repo-wide clean; frontend tsgo clean; TemplateLibrary jest 112 passed; MCP typecheck, tool schema snapshots and tool-name lint; hogli build:openapi committed; pre-push ci:preflight --strict 0 failures.

Gaps:

  • No real PostHog Code run. This devbox has no PostHog Code sandbox provider and no GitHub App credentials, so no task ran and no pull request opened against the scratch repository. The task's arguments and description are proven at the facade boundary and by the real Task row.
  • The evals have never run. products/workflows/evals/eval_code_managed_templates.py is written, discovered and unit-tested, but this devbox has no LLM gateway, Braintrust or Modal credentials. Run it where they exist, from a checkout of this branch in a flox shell:
    hogli evals eval_code_managed_templates                   # green: all three cases (Docker)
    hogli evals eval_code_managed_templates --provider modal  # same on Modal
    # red on the skill before #515:
    git checkout 85da5752479 -- products/workflows/skills/designing-email-templates/SKILL.md
    hogli evals eval_code_managed_templates --eval slack_message_names_code_managed_template
    git checkout HEAD -- products/workflows/skills/designing-email-templates/SKILL.md
  • Prod shape is not measured until production read access exists (2026-11-02).
  • CI on this fork PR waits for a maintainer to approve the run.

Release status

  • This change is behind a feature flag and is not available to users

Docs update

The docs section on changing a code-managed template, with the request-change error anchors, ships with PostHog/posthog.com#21306 (Silthus/posthog#507).

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code, Opus 5.5 wrote the code and the review fixes.

Built as three tickets of spec Silthus/posthog#502 (map Silthus/posthog#461), each test-first with /tdd, then reviewed as one PR with a QA swarm because it adds a billable task with repository write access to a public API and an MCP tool. Skills: /improving-drf-endpoints, /implementing-mcp-tools, /writing-evals, /writing-kea-logics, /writing-tests, /writing-user-facing-copy, /writing-pr-descriptions. Every screenshot and fixture uses invented data (example.com, Hedgebox). CodeRabbit is retired on this repository, so it did not run.

Decisions

Question Answer Who Where
Does "Run with PostHog Code" ship, and do PostHog AI and Slack route code-managed changes to a PR? Yes: one server action, "request a change", serves the web button, agents and any MCP client (Card 4 → b, widened) 👤 Human spec #502, revision 3
Who edits a code-managed template? The agent with the repository edits the file. "Request a change" is only for agents and people without it (spec #502, revision 4) 👤 Human #511
May a call from inside a PostHog Code sandbox start a task? No: the tasks product's rule holds, so request_change refuses it 👤 Human map #461, Sandbox rule
What do PostHog AI and the Slack bot do for a code-managed template? They hand the person the template link, where Run with PostHog Code opens the pull request 👤 Human #507
Does the eval gap block the PR? No: the evals are recorded as a gap with the run command 👤 Human #515
Where is the task's prompt built? The same server builder as the copied prompt, plus the change rules; no second builder 🤖 Agent spec #502
Which PostHog scopes does the task get? Read-only: it changes a file and opens a pull request, so it can't change the template in PostHog 🤖 Agent #513
How is an agent's sentence tied to the file? A sentence that doesn't start with the starter gets In `<path>`, change what this request asks for: in front 🤖 Agent #513
Which repository forms run? Only github.com/<owner>/<name>, the form posthog-node and the skill store; a bare owner/name is refused as unsupported 🤖 Agent #513
Which status does each refusal get? 403 for immutable and posthog_code_not_available (including used-up credits and an unverifiable access check), 400 for the rest; keeps the spec's seven codes 🤖 Agent #513
Where does the page learn that PostHog Code is available? desktop/access/, the decision the route checks; credits are left to the route's refusal 🤖 Agent #514
How does the page check GitHub? Any GitHub integration on the project; whether it reaches this repository stays the route's refusal 🤖 Agent #514
Where do refusals show? In the popover, next to the sentence, with the server's detail and fix; a toast only when the popover is closed 🤖 Agent #514, #517
How does the skill tell it runs inside PostHog Code? By POSTHOG_TASK_RUN_ID, which every sandbox sets 🤖 Agent #515
How does the uploaded source reach PostHog AI? One hidden untrusted item; the trusted instruction names it and quotes no uploaded value 🤖 Agent #515
Which workflow access does a person need? Editor, the platform's mapping of a :write scope; the button names it before the click. Replaced the spec's "read access"; the alternative is a custom per-action permission at viewer 🤖 Agent #517
Does a token need template read too? Yes: hog_flow:read next to task:write, as the spec says 🤖 Agent #517
Where does the GitHub repository rule live? Only in the route; the page checks that a repository is recorded. Two tickets had built the rule twice; the alternative was a new read-only field 🤖 Agent #517
How does the eval get the sidebar context? From a fixture the page's jest test renders with the real builders; two tickets had built the text twice 🤖 Agent #517
May an uploaded source pick the task's repository? Yes, as the spec sets: the page shows it before the click, and the task only opens a pull request. Upload validation stays out of scope 🤖 Agent #517
Does the MCP tool need the tasks product's agent-run flag? No: the spec opens it to MCP clients behind its own flag, as experiments' cleanup PR tool does 🤖 Agent #517
How does closing a chip on one template keep other pages' context? Each template page gets its own dismiss group, and its skill chip is not dismissible, like the new-template page's; closing it dismissed the shared skill key everywhere. Alternative: group-scoped dismissal in the shared context store 🤖 Agent #517
Which refusals count? Every one: message template change refused {template_id, surface, code}, including ownership and instruction refusals 🤖 Agent #517
What is the merge risk? High: a billable task that writes to the customer's repository; shadow diff run 🤖 Agent #517

Involvement: no grilling rounds for this stack; 1 prototype on the map (4 rounds); QA swarm with 2 panel rounds and 1 final read (GPT-6.1 Sol high: correctness, risk, design, systems, author perspective; Opus 5.5 high: fresh eyes, UI, agent documents); live run and real-app UI run captured; shadow diff run.

Closes Silthus#513
Closes Silthus#514
Closes Silthus#515
Closes Silthus#502

🤖 Generated with Claude Code

Silthus and others added 30 commits October 16, 2026 18:42
Seven nullable columns on the message template, and a partial unique
index on (team, key) for templates that are not deleted. The index is
built concurrently in its own non-atomic migration. Nothing reads or
writes the columns yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PUT /api/projects/{project_id}/messaging_templates/by_key/{key}/ creates the
template on the first upload (keyed, managed by code, transactional with the
default verified sender), publishes changed content after that, and answers
unchanged when nothing changed. Project secret API keys with hog_flow:write can
call it, behind workflows-transactional-email-api.

The update, design and upload routes now share one save-and-sync service that
writes the template, its content hash and the workflow version together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A template uploaded from code is replaced by its next upload, so an edit
made in PostHog was lost without warning. One ownership policy now
refuses every write to a template with managed_by = code except the
upload, sends, make transactional and a release. It answers 403
immutable with why, fix (naming the file when the source is known) and
docs_url.

The policy keys on the action and the whole payload, so a write action
added later is refused until someone allows it. It runs on the row read
by the permission check, and again on the row locked by the save path,
so an upload that claims the template mid-request still wins.

A PATCH whose only field is managed_by: gui releases the template. It
keeps its key, so the next upload claims it again. The MCP update and
design tool descriptions point to the upload tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntent hash

POST /api/projects/{project_id}/messaging/send/email/ names the template in
the body as {key, hash?} or {id} and sends it through the existing send
service, behind workflows-transactional-email-api. Project secret API keys
with hog_flow:write can call only this action.

A hash the template never published answers 409 template_outdated and sends
nothing. A hash newer content replaced sends the current content with a
template_superseded warning, so an old deploy never brings old content back.
The send usage event records how the template was addressed and how the
hash compared. A generated MCP tool exposes the send.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o spec/code-managed-templates/488

# Conflicts:
#	products/messaging/backend/facade/templates.py
#	products/messaging/backend/presentation/views/message_templates.py
A PATCH whose only field is key gives a keyless template its key, once,
so the next upload under that key takes it over and keeps its id,
workflow and sender. Refuses key_already_set, key_in_use,
invalid_template_key, and a key change sent with other fields.
Project secret API keys still cannot reach the PATCH. The MCP update
tool exposes the field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o spec/code-managed-templates/490

# Conflicts:
#	products/messaging/backend/presentation/views/message_templates.py
#	products/messaging/frontend/generated/api.schemas.ts
A template uploaded from code gets a "Managed by code" tag on its card and
page, a source line that links its file, repository and commit on known
hosts, and a notice that edits here can't be saved. A keyed template shows
its key, and the API tab sends through the send route by key or id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An upload with `if_not_superseded` set whose content hash is in the template's
superseded list writes nothing and answers `200 superseded`, naming the current
hash. Without the option an upload publishes as before, so an explicit upload
stays the rollback path. The MCP upload tool describes the option.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A code-managed template page now shows a read-only preview (subject,
preheader, HTML in a sandboxed frame, plain text) in place of the email
editor, with no Save, Discard or Save as new template. The form of a
code-managed template never diverges from the server copy and its submit
sends nothing, so neither a save nor any draft autosave can write it.

"Stop managing in code" in the page menu asks for confirmation, sends the
release PATCH and reloads the page as an editable template. Delete is
disabled on the page and the library card until the template is released.
Duplicate on both surfaces shares one copy payload that carries only the
name, description and content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An upload checks workflow editor access before it answers `unchanged`
  or `superseded`, so a member without access can't rewrite the source.
- An upload repairs a stored hash that does not describe the published
  content, so a save that skipped the hash no longer loops on
  `template_outdated`.
- Keys validate as whole strings, and the send no longer trims the key,
  so `welcome\n` is refused instead of stored or silently matched.
- The PATCH schema declares the release (`managed_by: gui`). The MCP
  update tool leaves it out.
- The shared transactional email API pieces move out of the template
  view into their own module. One PATCH matcher, one refusal base and
  one usage capture replace the pairs that adoption, release, upload
  and send each built. The key constraint name lives on the model.
- The line-ending hash case checks a fixed digest instead of the
  function under test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A template save no longer sends the key, owner or source, which only
  the server sets, so a key set elsewhere never blocks a save.
- Duplicate shows a loading state while it runs and the real error when
  it fails.
- Links in the read-only preview open in a new tab instead of replacing
  the preview.
- A long key truncates instead of overflowing the header.
- Source links: Bitbucket branch and commit paths, encoded branch names,
  and GitLab projects in subgroups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Links in the read-only preview open in a new tab even when the email
  sets its own target. The preview sets the target on every link and
  keeps the email's doctype. Its test goes through the rendered frame.
- An upload refused for missing workflow access reports its usage event.
- A long key truncates with an ellipsis and keeps its copy button.
- The single-field PATCH matcher moves from the facade contract into
  the ownership service, so the contract stays plain data.
- A GitLab remote with a trailing slash links to the right project.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A member who released a code-managed template and edited it could lose
the edits when an upload claimed the template back: the save answered
403 immutable, the page toasted and stayed editable until a reload.

Now a save refused with immutable reloads the template, so the page
turns read-only with the managed-by-code notice, and a warning banner
offers "Duplicate with my changes". The duplicate uses the shared copy
payload with the unsaved values, so it carries no key, ownership,
source or hash. Closing the banner discards the edits.

Refs #508

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The final review found two gaps in the refused-edits flow:

- A failed reload after `403 immutable` left the page editable, and
  closing the banner did not discard the edits. The logic now marks the
  loaded copy as managed by code and resets the form at once, then
  reloads. Only the offered duplicate keeps the refused edits.
- A duplicate still navigated to the copy after the person had left the
  page. Both duplicate listeners now check their breakpoint after the
  request, before they navigate.

Refs #508

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The template API returns change_prompt_starter and change_prompt_context
on a template managed by code, and null on any other. One builder in the
messaging product makes both, so the web page, PostHog AI, Slack and any
MCP client hand a coding agent the same text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…PostHog Code

POST messaging_templates/{id}/request_change/ with an instruction starts a PostHog Code
task that edits the template's file in its GitHub repository and opens a pull request.
PostHog never changes the template: the next upload of the file publishes it.

- Callers: session, personal API key and OAuth with task:write. Project secret API keys
  and requests from inside a PostHog Code sandbox are refused.
- Gates: the transactional email API flag, PostHog Code access and credits, a GitHub
  repository the project's integration reaches, and a template managed by code.
- The task gets read-only PostHog MCP scopes, so it can't write the template either.
- New MCP tool workflows-request-email-template-change. The 403 immutable fix and the
  update, patch and get tool descriptions send agents to the file first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…our agent

A code-managed template shows "Edit with your agent" in the Save slot. It opens a
popover where the person completes the server's starter sentence and copies it,
followed by a blank line and the server's change prompt context. Nothing is saved.

Copy waits until the sentence describes a change. The sentence stays in page state
while the popover closes and reopens. The notice now points at the file or a
coding agent.

Events: agent prompt opened and copied, template duplicated, and released from
code. No prompt text, path or repository goes into their properties.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…popover

A code-managed template's "Edit with your agent" popover gains "Run with
PostHog Code" next to "Copy prompt". It sends the sentence as typed to the
template's request_change route, which starts a PostHog Code task that opens
a pull request. PostHog doesn't change the template.

- The button shows only when the project can use PostHog Code (the desktop
  access check the route uses).
- It is disabled until the template has a GitHub repository, the project has
  a GitHub integration, and the change is described.
- One request at a time: a second click while the request is in flight sends
  nothing.
- On success a toast and the code-managed notice link the task, the sentence
  goes back to the starter, and the popover closes.
- On a refusal the popover shows the server's detail and fix, and keeps the
  sentence and Copy prompt.
- Events: message template change run requested {template_id} and message
  template change run failed {template_id, code}.

The code-managed notice moves to its own component, so a story can show it
with the task link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The side menu of "Edit with your agent" offers "Copy HTML file". It copies
the published template as the exact file posthog-node's templateFile()
reads: the subject goes into the first <title> and the preheader into the
first <meta name="preheader">, each added when missing. A tag whose value
already matches stays as it is, so a template uploaded from a file copies
back byte for byte. A blank preheader removes its tag, and Liquid stays
literal.

Nothing is saved. The toast says how the file goes live, and notes when
the saved template has a plain-text part the file can't carry.

Event: message template html file copied {template_id, dropped_text_part}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-agent-edit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…of the change prompt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ble list

The lazy regex rescanned the rest of the HTML after every unclosed opener, so a large upload made
every list and retrieve quadratic. change_prompt_variables gives the page the server's list, so the
frontend no longer rescans the prompt's prose for its usage event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus and others added 11 commits October 17, 2026 10:56
…and keep focus on the agent button

- variable_count reads change_prompt_variables instead of rescanning the prompt's prose
- Duplicate with my changes and the library card's Duplicate fire message template duplicated
- a reload that points the template at another file drops a sentence typed for the old file
- the trigger announces its popover and gets focus back when the popover closes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scroll container inside the collapse inside the popover's 3D transform stays unpainted in
Chromium until it scrolls, so the prompt now grows inside the popover's own scroll area.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only whitespace, backticks and control characters can carry an instruction in a path, so paths like
app/(marketing)/emails/[locale]/welcome.html stay known.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… full prompt

- focus still inside the closing prompt goes back to the trigger; the popover keeps its content
  mounted for its exit, so the earlier body-only check never fired
- the prompt panel is a labelled dialog, and Copy prompt sits above Show full prompt
- the full prompt is a div, since global styles make every pre a scroll container
- a blank preheader removes every preheader tag in one pass

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…prompt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s focus

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pec/template-request-change/515

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…emplate tools

A sandboxed case can now set `prompt_from_seed` to build its first prompt from
the `setup` result. A suite that replays the page context a product attaches
needs this, because that context names a seeded id. The agent and the scorers
get the built prompt; `prompt` stays the static case input.

The eval MCP server now forces `workflows-transactional-email-api` on, so it
serves the code-managed email template tools that Django already serves under
the harness's flag stub.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot in PostHog

- Skill: `designing-email-templates` gains "Code-managed templates". An agent
  checks `managed_by`, never calls the update or patch tool on a code-managed
  template, and changes the file where it lives: itself when it can reach the
  repository, through the template's page ("Run with PostHog Code") when it runs
  inside PostHog Code, and through `workflows-request-email-template-change`
  otherwise.
- Page context: a code-managed template's page attaches the template, its key
  and source as untrusted items, and a static instruction that points at the
  skill section and the page's buttons.
- Tool description: the request tool says it refuses calls from inside PostHog
  Code, and what to do there instead.
- Evals: `workflows/eval_code_managed_templates` runs the sidebar, a Slack
  message and a template made in PostHog against a seeded project.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@trunk-io

trunk-io Bot commented Oct 17, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

Silthus and others added 5 commits October 17, 2026 16:14
- request_change needs hog_flow:read next to task:write, so a token that
  can't read workflow templates can't start a task on one.
- The 403 immutable fix and the update/patch tool texts point at
  change_prompt_context for how a change goes live, add the PostHog Code
  branch, and name the file only when its uploaded path has a file's shape.
- A source path with a `..` segment counts as unknown in the change prompt.
- The task description no longer contradicts the by-key upload line.
- The sandbox refusal names the page steps; not-connected mentions delay.
- Refusals send `message template change refused` {template_id, surface, code}.
- The skill forbids rebuilding and uploading a code-managed template from
  PostHog, and keeps check-outs to agents outside PostHog Code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The button names the missing workflow editor access before the click,
  instead of a 403 after it, and refuses a change longer than the route
  takes, using the generated limit.
- The page checks only that the template records a repository; the route
  owns which repositories PostHog Code can work in, so the GitHub rule
  lives in one place.
- A refusal that arrives after the popover closed shows as a toast.
- Editing the sentence clears an old refusal; long names wrap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d template eval

- The sidebar case reads a fixture that templateAgentContext.test.ts renders
  from the page's own context builder, instead of a hand copy in Python.
  The seeder test checks the fixture names the seeded template.
- Each code-managed template page gets its own dismiss group, so closing one
  page's chip no longer strips the context from every other.
- The scorer counts an upload by the template's key as touching it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `message template change refused` now also fires for a template made in
  PostHog and for a missing or oversized instruction.
- The 403 immutable fix and every tool text use one wording for when an agent
  edits the file itself: the repository is in its workspace, or it runs
  outside PostHog Code and can check it out.
- The upload tool says to upload a code-managed template only from its file.
- Refusal fixes point at change_prompt_context instead of "then upload it",
  and `template_repository_unsupported` names only a repository-shaped value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e the route

- The code-managed page's skill chip is not dismissible, like the new-template
  page's: closing it dismissed the shared skill key on every page.
- The 2,000-character check counts code points, as the route does.
- A discarded sentence drops its refusal banner.
- The eval fixture ends with a newline; the seeder test ties its name line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant