From 053e674e023827eb6c9ed405215738c499858049 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 06:22:41 +0000 Subject: [PATCH 1/8] fix(automations): Tell the agent to write the creator mention for "me" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A due reminder such as "Remind me to do healthchecks" often posts "Reminder: Do your healthchecks." without the creator mention, so Slack notifies nobody. #2052 changed the creator line in task input from a rule about what to write ("Where the instructions say "me" or "my", write this mention.") to a rule about what "me" means ("Me" and "my" mean this person: mention them in the message.). With only the meaning, the model often resolves "me" to "your" and drops the mention. The task input also reaches the model XML-escaped inside ``, so the mention shows as `<@U…>`, which makes the drop more likely. The creator line now says what to write again, and it keeps the rule to not mention the creator when the instructions do not say "me" or "my". Co-Authored-By: David Cramer --- packages/junior-dashboard/src/mock-reporting/fixtures.ts | 2 +- packages/junior/src/chat/task-input.ts | 4 +++- packages/junior/tests/integration/event-automations.test.ts | 2 +- packages/junior/tests/unit/chat/task-input.test.ts | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/junior-dashboard/src/mock-reporting/fixtures.ts b/packages/junior-dashboard/src/mock-reporting/fixtures.ts index 0fb1d93de2..46f6de4e29 100644 --- a/packages/junior-dashboard/src/mock-reporting/fixtures.ts +++ b/packages/junior-dashboard/src/mock-reporting/fixtures.ts @@ -1451,7 +1451,7 @@ function eventAutomationRunConversation( "This is a task, not a message from a person.", "", "About: Issue · ACME-42", - 'Created by: <@U123>. "Me" and "my" in the instructions mean this person: mention them in the message. If the instructions do not say "me" or "my", do not mention this person.', + 'Created by: <@U123>. Where the instructions say "me" or "my", write this mention. If they do not say "me" or "my", do not mention this person.', "Instructions: Summarize the closed issue", "", "Trusted summary: ACME-42 Checkout fails for saved cards was closed by Morgan Lee.", diff --git a/packages/junior/src/chat/task-input.ts b/packages/junior/src/chat/task-input.ts index 5dc1610516..df42fa07f1 100644 --- a/packages/junior/src/chat/task-input.ts +++ b/packages/junior/src/chat/task-input.ts @@ -103,9 +103,11 @@ export function renderTaskInput(args: { "This is a task, not a message from a person.", "", ...(about ? [`About: ${oneLine(about)}`] : []), + // Tell the model what to write, not what "me" means. With only the + // meaning, the model often writes "your" and drops the mention. ...(creator ? [ - `Created by: ${oneLine(creator)}. "Me" and "my" in the instructions mean this person: mention them in the message. If the instructions do not say "me" or "my", do not mention this person.`, + `Created by: ${oneLine(creator)}. Where the instructions say "me" or "my", write this mention. If they do not say "me" or "my", do not mention this person.`, ] : []), ...(people.length > 0 ? [`People involved: ${people.join(", ")}`] : []), diff --git a/packages/junior/tests/integration/event-automations.test.ts b/packages/junior/tests/integration/event-automations.test.ts index 71ae89495a..d83ac5e8b0 100644 --- a/packages/junior/tests/integration/event-automations.test.ts +++ b/packages/junior/tests/integration/event-automations.test.ts @@ -406,7 +406,7 @@ describe("event automations", () => { This is a task, not a message from a person. About: GitHub PR getsentry/junior#1174 - Created by: <@U123>. "Me" and "my" in the instructions mean this person: mention them in the message. If the instructions do not say "me" or "my", do not mention this person. + Created by: <@U123>. Where the instructions say "me" or "my", write this mention. If they do not say "me" or "my", do not mention this person. Instructions: Address the requested changes. Trusted summary: A reviewer requested changes. diff --git a/packages/junior/tests/unit/chat/task-input.test.ts b/packages/junior/tests/unit/chat/task-input.test.ts index a71b2a1052..a3095633de 100644 --- a/packages/junior/tests/unit/chat/task-input.test.ts +++ b/packages/junior/tests/unit/chat/task-input.test.ts @@ -36,7 +36,7 @@ describe("renderTaskInput", () => { This is a task, not a message from a person. About: GitHub PR getsentry/junior#691 - Created by: <@U123>. "Me" and "my" in the instructions mean this person: mention them in the message. If the instructions do not say "me" or "my", do not mention this person. + Created by: <@U123>. Where the instructions say "me" or "my", write this mention. If they do not say "me" or "my", do not mention this person. People involved: <@U456> Done by: <@U456>. This person did what this Event describes. The Event calls them "octocat". That is the same person. They are also in People involved. Write to them as "you", and do not also call them "octocat". Instructions: Fix failed checks on this PR. From 33aa18fa843d3ea5eab61dfc262b050192f27abd Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 16:22:57 +0000 Subject: [PATCH 2/8] fix(automations): Bind "me" to the creator identity at run time Stored instructions keep the creator's own words. Each run reads the creator identity and renders its mention for the Destination, instead of using the createdBy copy or a mention saved in the instruction text. Co-Authored-By: David Cramer --- .../src/mock-reporting/fixtures.ts | 2 +- .../integration/scheduler/creation.eval.ts | 9 ++--- packages/junior/src/chat/README.md | 2 +- .../junior/src/chat/automations/README.md | 6 +++- .../junior/src/chat/automations/creator.ts | 34 +++++++++++++++++++ .../src/chat/automations/edit-schema.ts | 5 +-- .../src/chat/event-automations/ingest.ts | 13 +++++-- .../chat/scheduled-automations/heartbeat.ts | 11 +++--- packages/junior/src/chat/task-input.ts | 9 +++-- .../integration/event-automations.test.ts | 2 +- .../heartbeat-scheduled-automations.test.ts | 4 ++- .../junior/tests/unit/chat/task-input.test.ts | 2 +- 12 files changed, 77 insertions(+), 22 deletions(-) diff --git a/packages/junior-dashboard/src/mock-reporting/fixtures.ts b/packages/junior-dashboard/src/mock-reporting/fixtures.ts index 46f6de4e29..e84db82b2d 100644 --- a/packages/junior-dashboard/src/mock-reporting/fixtures.ts +++ b/packages/junior-dashboard/src/mock-reporting/fixtures.ts @@ -1451,7 +1451,7 @@ function eventAutomationRunConversation( "This is a task, not a message from a person.", "", "About: Issue · ACME-42", - 'Created by: <@U123>. Where the instructions say "me" or "my", write this mention. If they do not say "me" or "my", do not mention this person.', + 'Created by: <@U123>. "Me" and "my" in the instructions mean this person. Where the message addresses them, write this mention. If the instructions do not say "me" or "my", do not mention this person.', "Instructions: Summarize the closed issue", "", "Trusted summary: ACME-42 Checkout fails for saved cards was closed by Morgan Lee.", diff --git a/packages/junior-evals/evals/integration/scheduler/creation.eval.ts b/packages/junior-evals/evals/integration/scheduler/creation.eval.ts index efd794cb5c..0d8f365b79 100644 --- a/packages/junior-evals/evals/integration/scheduler/creation.eval.ts +++ b/packages/junior-evals/evals/integration/scheduler/creation.eval.ts @@ -76,7 +76,7 @@ describe("Schedule Creation", () => { expect(creates[0]!.input).not.toHaveProperty("nextRunAt"); }); - test("when a reminder for the requester posts to the channel, name the requester exactly", async ({ + test("when a reminder for the requester posts to the channel, keep their own words for them", async ({ run, }) => { const conversation = await run( @@ -90,10 +90,11 @@ describe("Schedule Creation", () => { conversation, ); expect(creates).toHaveLength(1); - // The task runs later without this request, so "me" must become a mention. + // Each run binds "me" and "my" to the creator identity, so the saved + // instruction keeps the requester's words instead of a stored mention. const { instruction } = creates[0]!.input as { instruction?: string }; - expect(instruction).toContain("<@U0TEST>"); - expect(instruction).not.toMatch(/\b(?:me|my)\b/i); + expect(instruction).toMatch(/\b(?:me|my)\b/i); + expect(instruction).not.toContain("<@U0TEST>"); }); test("when asked to tell the channel something later, preserve the future work in the schedule", async ({ diff --git a/packages/junior/src/chat/README.md b/packages/junior/src/chat/README.md index 3098ba529a..1666a3710c 100644 --- a/packages/junior/src/chat/README.md +++ b/packages/junior/src/chat/README.md @@ -329,7 +329,7 @@ so it has no instructions of its own. `agent-invocations/result.ts` owns its | 1 | `[task]` | yes | Task header. Same for schedule, event, and subscription. | | 2 | Origin | yes | `This is a task, not a message from a person.` | | 3 | `About:` | no | One-line resource label. | -| 4 | `Created by:` | no | Creator mention, and the rule to mention them only for "me" or "my". | +| 4 | `Created by:` | no | Creator mention made from the creator identity. Binds "me" and "my". | | 5 | `People involved:` | no | Event people mentions. Guidance says when to address them. | | 6 | `Done by:` | no | Linked person who did the Event, tied to its provider handle. | | 7 | `Instructions:` | yes | Stored task text or subscription intent. | diff --git a/packages/junior/src/chat/automations/README.md b/packages/junior/src/chat/automations/README.md index bb18735940..4ca2b111fb 100644 --- a/packages/junior/src/chat/automations/README.md +++ b/packages/junior/src/chat/automations/README.md @@ -86,7 +86,11 @@ instruction or event selector switches creator credential use to system credentials. The edit read returns `ownedByViewer` so the editor can show these rules before a save. -An Automation runs later without the request that created it. The agent tools +An Automation runs later without the request that created it. The instruction +keeps "me" and "my" for the creator. Each run reads the creator identity and +renders its mention for the Destination in the `Created by:` task input line. +The `createdBy` copy does not decide this. Other people need an exact mention +in the instruction, because the request is gone at run time. The agent tools that create or update an Automation reject an instruction that names the requester by display name without their Slack mention. A name does not notify the person. diff --git a/packages/junior/src/chat/automations/creator.ts b/packages/junior/src/chat/automations/creator.ts index 7a041b26ae..fcc0cdabfa 100644 --- a/packages/junior/src/chat/automations/creator.ts +++ b/packages/junior/src/chat/automations/creator.ts @@ -1,4 +1,7 @@ import type { User } from "@sentry/junior-plugin-api"; +import { readIdentityByIdFromSql } from "@/chat/plugins/viewer"; +import { slackMention } from "@/chat/slack/mrkdwn"; +import type { JuniorDatabase } from "@/db/db"; /** * Return whether the user created the Automation. Both kinds store the @@ -12,3 +15,34 @@ export function isAutomationCreator( (identity) => identity.id === automation.creatorIdentityId, ); } + +/** + * Render the Automation creator for a run in its Destination. + * + * The creator identity is the source of truth, not the `createdBy` copy. The + * create tools store the creator's Slack identity in the Destination + * workspace, and the row cannot be deleted while the Automation exists. A + * creator that does not resolve there is a broken invariant, so the run fails + * instead of binding "me" to nobody or to the wrong person. + */ +export async function automationCreatorMention( + db: JuniorDatabase, + automation: { + id: string; + creatorIdentityId: string; + destination: { teamId: string }; + }, +): Promise { + const identity = ( + await readIdentityByIdFromSql(db, automation.creatorIdentityId) + )?.identity; + if ( + identity?.provider !== "slack" || + identity.providerTenantId !== automation.destination.teamId + ) { + throw new Error( + `Automation ${automation.id} creator identity does not resolve in its Destination workspace.`, + ); + } + return slackMention(identity.providerSubjectId); +} diff --git a/packages/junior/src/chat/automations/edit-schema.ts b/packages/junior/src/chat/automations/edit-schema.ts index 728dccd9a6..bf0d5fedf4 100644 --- a/packages/junior/src/chat/automations/edit-schema.ts +++ b/packages/junior/src/chat/automations/edit-schema.ts @@ -12,11 +12,12 @@ export const automationInstructionSchema = z.string().trim().min(1).max(4000); /** * Instruction input for agent tools. An Automation runs later without the - * request around it, so the instruction must name each person exactly. + * request around it. "Me" and "my" stay as written, because each run binds + * them to the creator identity. Other people need an exact mention. */ export const automationInstructionToolSchema = automationInstructionSchema.describe( - "Work to perform when the Automation runs. It runs later without this conversation, so write it to stand alone: name each person with their Slack mention, such as <@U123>, instead of me, my, or you. Preserve the user's scope, targets, and constraints. Do not add permissions or side effects the user did not request.", + 'Work to perform when the Automation runs. It runs later without this conversation, so write it to stand alone. Keep "me" and "my" where they mean the Automation creator; each run binds them to the creator. Name every other person with their Slack mention, such as <@U123>, including a requester who is not the creator. Preserve the user\'s scope, targets, and constraints. Do not add permissions or side effects the user did not request.', ); export const automationEditFieldsSchema = z.object({ diff --git a/packages/junior/src/chat/event-automations/ingest.ts b/packages/junior/src/chat/event-automations/ingest.ts index f284e6a317..02c7c94c82 100644 --- a/packages/junior/src/chat/event-automations/ingest.ts +++ b/packages/junior/src/chat/event-automations/ingest.ts @@ -4,7 +4,6 @@ * Each matching automation is independently idempotent. Aggregate failures propagate * so the provider can retry the original delivery. */ -import { slackMention } from "@/chat/slack/mrkdwn"; import { createHash } from "node:crypto"; import { EVENT_SUMMARY_MAX_LENGTH, @@ -14,6 +13,7 @@ import { type TaskOutcome, } from "@sentry/junior-plugin-api"; import { dispatchEventAutomation } from "@/chat/agent-dispatch/context"; +import { automationCreatorMention } from "@/chat/automations/creator"; import { automationReplyAttribution } from "@/chat/automations/reply-attribution"; import { renderTaskInput } from "@/chat/task-input"; import { effectiveTaskOutcomes } from "@/chat/task-outcomes"; @@ -55,6 +55,7 @@ function eventAutomationDispatchKey( /** Render plain agent input for one matching event automation. */ function eventInput( task: EventAutomation, + creator: string, event: Event, outcomes: TaskOutcome[], catalog: EventCatalog, @@ -62,7 +63,7 @@ function eventInput( const guidance = eventGuidance(catalog, event.namespace, event.eventType); return renderTaskInput({ about: task.trigger.label, - creator: slackMention(task.createdBy.slackUserId), + creator, instructions: task.task.text, outcomes, guidance, @@ -168,7 +169,13 @@ export async function ingestEventAutomations( ...(credentialSubject ? { credentialSubject } : undefined), destination, destinationVisibility: task.destinationVisibility, - input: eventInput(task, event, outcomes, catalog), + input: eventInput( + task, + await automationCreatorMention(db, task), + event, + outcomes, + catalog, + ), metadata: { eventAutomationId: task.id }, replyAttribution: automationReplyAttribution(task.title), outcomes, diff --git a/packages/junior/src/chat/scheduled-automations/heartbeat.ts b/packages/junior/src/chat/scheduled-automations/heartbeat.ts index 55bb73b551..4263e92b4a 100644 --- a/packages/junior/src/chat/scheduled-automations/heartbeat.ts +++ b/packages/junior/src/chat/scheduled-automations/heartbeat.ts @@ -1,4 +1,3 @@ -import { slackMention } from "@/chat/slack/mrkdwn"; import { type Dispatch } from "@sentry/junior-plugin-api"; import { dispatchScheduledAutomation, @@ -10,6 +9,7 @@ import { getDb } from "@/chat/db"; import { logInfo } from "@/chat/logging"; import type { ConversationWorkQueue } from "@/chat/task-execution/queue"; import { notifyAutomationBlocked } from "@/chat/automations/blocked-notice"; +import { automationCreatorMention } from "@/chat/automations/creator"; import { settleEventAutomationRuns } from "@/chat/event-automations/runs"; import { automationReplyAttribution } from "@/chat/automations/reply-attribution"; import type { JuniorDatabase } from "@/db/db"; @@ -40,9 +40,12 @@ function singleLineMetadataValue(value: string): string { } /** Render the due scheduled automation as plain agent input. */ -function buildDispatchInput(task: ScheduledAutomation): string { +async function buildDispatchInput( + db: JuniorDatabase, + task: ScheduledAutomation, +): Promise { return renderTaskInput({ - creator: slackMention(task.createdBy.slackUserId), + creator: await automationCreatorMention(db, task), instructions: task.task.text, outcomes: effectiveTaskOutcomes(task.outcomes, task.destination), }); @@ -381,7 +384,7 @@ export async function runScheduledAutomationHeartbeat(args: { : undefined), destination: task.destination, destinationVisibility: task.conversationAccess.visibility, - input: buildDispatchInput(task), + input: await buildDispatchInput(db, task), metadata, replyAttribution: automationReplyAttribution(task.title), outcomes: effectiveTaskOutcomes(task.outcomes, task.destination), diff --git a/packages/junior/src/chat/task-input.ts b/packages/junior/src/chat/task-input.ts index df42fa07f1..1516901106 100644 --- a/packages/junior/src/chat/task-input.ts +++ b/packages/junior/src/chat/task-input.ts @@ -70,7 +70,10 @@ export function renderTaskInput(args: { outcomes?: TaskOutcome[]; /** Human label for the matched resource, when present. */ about?: string; - /** Mention of the person who created the task, so "me" in it is exact. */ + /** + * Creator mention for the Destination, rendered from the creator identity + * at run time. Stored instructions keep "me" and "my"; this binds them. + */ creator?: string; /** Mentions of the people involved in an Event. Guidance says when to address them. */ people?: readonly string[]; @@ -103,11 +106,11 @@ export function renderTaskInput(args: { "This is a task, not a message from a person.", "", ...(about ? [`About: ${oneLine(about)}`] : []), - // Tell the model what to write, not what "me" means. With only the + // Bind "me" to the creator, then say what to write. With only the // meaning, the model often writes "your" and drops the mention. ...(creator ? [ - `Created by: ${oneLine(creator)}. Where the instructions say "me" or "my", write this mention. If they do not say "me" or "my", do not mention this person.`, + `Created by: ${oneLine(creator)}. "Me" and "my" in the instructions mean this person. Where the message addresses them, write this mention. If the instructions do not say "me" or "my", do not mention this person.`, ] : []), ...(people.length > 0 ? [`People involved: ${people.join(", ")}`] : []), diff --git a/packages/junior/tests/integration/event-automations.test.ts b/packages/junior/tests/integration/event-automations.test.ts index d83ac5e8b0..72bb4d243a 100644 --- a/packages/junior/tests/integration/event-automations.test.ts +++ b/packages/junior/tests/integration/event-automations.test.ts @@ -406,7 +406,7 @@ describe("event automations", () => { This is a task, not a message from a person. About: GitHub PR getsentry/junior#1174 - Created by: <@U123>. Where the instructions say "me" or "my", write this mention. If they do not say "me" or "my", do not mention this person. + Created by: <@U123>. "Me" and "my" in the instructions mean this person. Where the message addresses them, write this mention. If the instructions do not say "me" or "my", do not mention this person. Instructions: Address the requested changes. Trusted summary: A reviewer requested changes. diff --git a/packages/junior/tests/integration/heartbeat-scheduled-automations.test.ts b/packages/junior/tests/integration/heartbeat-scheduled-automations.test.ts index b0cd86fd2a..b55625a5bc 100644 --- a/packages/junior/tests/integration/heartbeat-scheduled-automations.test.ts +++ b/packages/junior/tests/integration/heartbeat-scheduled-automations.test.ts @@ -200,7 +200,9 @@ describe("scheduled automation heartbeat", () => { const dispatchRecord = await getDispatchRecord(running!.dispatchId!); expect(dispatchRecord?.input).toContain("[task]"); expect(dispatchRecord?.input).not.toContain("[[NO_REPLY]]"); - expect(dispatchRecord?.input).toContain("Created by: <@U039RR91S>"); + // The creator identity decides the mention, not the createdBy copy. + expect(dispatchRecord?.input).toContain("Created by: <@U123>"); + expect(dispatchRecord?.input).not.toContain("U039RR91S"); expect(dispatchRecord?.input).toContain( "Instructions: Post a digest. Summarize the latest state.", ); diff --git a/packages/junior/tests/unit/chat/task-input.test.ts b/packages/junior/tests/unit/chat/task-input.test.ts index a3095633de..f2adf121d0 100644 --- a/packages/junior/tests/unit/chat/task-input.test.ts +++ b/packages/junior/tests/unit/chat/task-input.test.ts @@ -36,7 +36,7 @@ describe("renderTaskInput", () => { This is a task, not a message from a person. About: GitHub PR getsentry/junior#691 - Created by: <@U123>. Where the instructions say "me" or "my", write this mention. If they do not say "me" or "my", do not mention this person. + Created by: <@U123>. "Me" and "my" in the instructions mean this person. Where the message addresses them, write this mention. If the instructions do not say "me" or "my", do not mention this person. People involved: <@U456> Done by: <@U456>. This person did what this Event describes. The Event calls them "octocat". That is the same person. They are also in People involved. Write to them as "you", and do not also call them "octocat". Instructions: Fix failed checks on this PR. From 3cb6f35724004203a5bacc6132a2419c7e5644a7 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 16:23:42 +0000 Subject: [PATCH 3/8] fix(automations): Keep the concrete write rule in the creator line Co-Authored-By: David Cramer --- packages/junior-dashboard/src/mock-reporting/fixtures.ts | 2 +- packages/junior/src/chat/task-input.ts | 4 ++-- packages/junior/tests/integration/event-automations.test.ts | 2 +- packages/junior/tests/unit/chat/task-input.test.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/junior-dashboard/src/mock-reporting/fixtures.ts b/packages/junior-dashboard/src/mock-reporting/fixtures.ts index e84db82b2d..b86ef4c8ae 100644 --- a/packages/junior-dashboard/src/mock-reporting/fixtures.ts +++ b/packages/junior-dashboard/src/mock-reporting/fixtures.ts @@ -1451,7 +1451,7 @@ function eventAutomationRunConversation( "This is a task, not a message from a person.", "", "About: Issue · ACME-42", - 'Created by: <@U123>. "Me" and "my" in the instructions mean this person. Where the message addresses them, write this mention. If the instructions do not say "me" or "my", do not mention this person.', + 'Created by: <@U123>. "Me" and "my" in the instructions mean this person. Where the instructions say "me" or "my", write this mention. If they do not say "me" or "my", do not mention this person.', "Instructions: Summarize the closed issue", "", "Trusted summary: ACME-42 Checkout fails for saved cards was closed by Morgan Lee.", diff --git a/packages/junior/src/chat/task-input.ts b/packages/junior/src/chat/task-input.ts index 1516901106..e21774f42f 100644 --- a/packages/junior/src/chat/task-input.ts +++ b/packages/junior/src/chat/task-input.ts @@ -107,10 +107,10 @@ export function renderTaskInput(args: { "", ...(about ? [`About: ${oneLine(about)}`] : []), // Bind "me" to the creator, then say what to write. With only the - // meaning, the model often writes "your" and drops the mention. + // binding, the model often writes "your" and drops the mention. ...(creator ? [ - `Created by: ${oneLine(creator)}. "Me" and "my" in the instructions mean this person. Where the message addresses them, write this mention. If the instructions do not say "me" or "my", do not mention this person.`, + `Created by: ${oneLine(creator)}. "Me" and "my" in the instructions mean this person. Where the instructions say "me" or "my", write this mention. If they do not say "me" or "my", do not mention this person.`, ] : []), ...(people.length > 0 ? [`People involved: ${people.join(", ")}`] : []), diff --git a/packages/junior/tests/integration/event-automations.test.ts b/packages/junior/tests/integration/event-automations.test.ts index 72bb4d243a..a6e11a0fc7 100644 --- a/packages/junior/tests/integration/event-automations.test.ts +++ b/packages/junior/tests/integration/event-automations.test.ts @@ -406,7 +406,7 @@ describe("event automations", () => { This is a task, not a message from a person. About: GitHub PR getsentry/junior#1174 - Created by: <@U123>. "Me" and "my" in the instructions mean this person. Where the message addresses them, write this mention. If the instructions do not say "me" or "my", do not mention this person. + Created by: <@U123>. "Me" and "my" in the instructions mean this person. Where the instructions say "me" or "my", write this mention. If they do not say "me" or "my", do not mention this person. Instructions: Address the requested changes. Trusted summary: A reviewer requested changes. diff --git a/packages/junior/tests/unit/chat/task-input.test.ts b/packages/junior/tests/unit/chat/task-input.test.ts index f2adf121d0..a1ddcce016 100644 --- a/packages/junior/tests/unit/chat/task-input.test.ts +++ b/packages/junior/tests/unit/chat/task-input.test.ts @@ -36,7 +36,7 @@ describe("renderTaskInput", () => { This is a task, not a message from a person. About: GitHub PR getsentry/junior#691 - Created by: <@U123>. "Me" and "my" in the instructions mean this person. Where the message addresses them, write this mention. If the instructions do not say "me" or "my", do not mention this person. + Created by: <@U123>. "Me" and "my" in the instructions mean this person. Where the instructions say "me" or "my", write this mention. If they do not say "me" or "my", do not mention this person. People involved: <@U456> Done by: <@U456>. This person did what this Event describes. The Event calls them "octocat". That is the same person. They are also in People involved. Write to them as "you", and do not also call them "octocat". Instructions: Fix failed checks on this PR. From 39d05197d6dd72afae05ec5dc659b10019feb538 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 16:30:25 +0000 Subject: [PATCH 4/8] fix(automations): Say what a stand-alone instruction replaces The instruction tool description said "write it to stand alone" without saying what that means. A reference such as "this channel", "here", "this thread", or "that PR" needs the request to make sense. The run does not see the request. The description now tells the agent to replace each such reference with the exact target, such as a channel mention, a link, or an ID. --- packages/junior/src/chat/automations/edit-schema.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/junior/src/chat/automations/edit-schema.ts b/packages/junior/src/chat/automations/edit-schema.ts index bf0d5fedf4..92f79482d5 100644 --- a/packages/junior/src/chat/automations/edit-schema.ts +++ b/packages/junior/src/chat/automations/edit-schema.ts @@ -12,12 +12,13 @@ export const automationInstructionSchema = z.string().trim().min(1).max(4000); /** * Instruction input for agent tools. An Automation runs later without the - * request around it. "Me" and "my" stay as written, because each run binds - * them to the creator identity. Other people need an exact mention. + * request around it, so a reference to that request, such as "this channel", + * must become the exact target. "Me" and "my" stay as written, because each + * run binds them to the creator identity. Other people need an exact mention. */ export const automationInstructionToolSchema = automationInstructionSchema.describe( - 'Work to perform when the Automation runs. It runs later without this conversation, so write it to stand alone. Keep "me" and "my" where they mean the Automation creator; each run binds them to the creator. Name every other person with their Slack mention, such as <@U123>, including a requester who is not the creator. Preserve the user\'s scope, targets, and constraints. Do not add permissions or side effects the user did not request.', + 'Work to perform when the Automation runs. The run does not see this conversation, so the instruction must make sense without it. Replace each reference to this conversation, such as "this channel", "here", "this thread", or "that PR", with the exact target, such as <#C123>, a link, or an ID. Keep "me" and "my" where they mean the Automation creator; each run binds them to the creator. Name every other person with their Slack mention, such as <@U123>, including a requester who is not the creator. Preserve the user\'s scope, targets, and constraints. Do not add permissions or side effects the user did not request.', ); export const automationEditFieldsSchema = z.object({ From ce364069926928a3421b2e67e8276027a7744096 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 16:53:12 +0000 Subject: [PATCH 5/8] fix(automations): Call the creator "the creator" in saved instructions The run does not see the request, so "me" means nothing in a saved instruction. Tools now save "the creator", and each run binds it to the creator identity. The creator line still treats "me", "my", and a stored creator mention as the creator, so older instructions keep working. Co-Authored-By: David Cramer --- .../src/mock-reporting/fixtures.ts | 2 +- .../integration/scheduler/creation.eval.ts | 9 +++++---- .../integration/scheduler/delivery.eval.ts | 18 ++++++++++++++++++ packages/junior/src/chat/README.md | 2 +- packages/junior/src/chat/automations/README.md | 13 ++++++++----- .../junior/src/chat/automations/edit-schema.ts | 8 +++----- packages/junior/src/chat/task-input.ts | 9 +++++---- .../integration/event-automations.test.ts | 2 +- .../junior/tests/unit/chat/task-input.test.ts | 2 +- 9 files changed, 43 insertions(+), 22 deletions(-) diff --git a/packages/junior-dashboard/src/mock-reporting/fixtures.ts b/packages/junior-dashboard/src/mock-reporting/fixtures.ts index b86ef4c8ae..ae7f7cdd2f 100644 --- a/packages/junior-dashboard/src/mock-reporting/fixtures.ts +++ b/packages/junior-dashboard/src/mock-reporting/fixtures.ts @@ -1451,7 +1451,7 @@ function eventAutomationRunConversation( "This is a task, not a message from a person.", "", "About: Issue · ACME-42", - 'Created by: <@U123>. "Me" and "my" in the instructions mean this person. Where the instructions say "me" or "my", write this mention. If they do not say "me" or "my", do not mention this person.', + 'Created by: <@U123>. This person is the creator. Where the instructions refer to the creator, write this mention. "Me", "my", and this mention in the instructions also mean the creator. If the instructions do not refer to the creator, do not mention this person.', "Instructions: Summarize the closed issue", "", "Trusted summary: ACME-42 Checkout fails for saved cards was closed by Morgan Lee.", diff --git a/packages/junior-evals/evals/integration/scheduler/creation.eval.ts b/packages/junior-evals/evals/integration/scheduler/creation.eval.ts index 0d8f365b79..6c0a8d0b15 100644 --- a/packages/junior-evals/evals/integration/scheduler/creation.eval.ts +++ b/packages/junior-evals/evals/integration/scheduler/creation.eval.ts @@ -76,7 +76,7 @@ describe("Schedule Creation", () => { expect(creates[0]!.input).not.toHaveProperty("nextRunAt"); }); - test("when a reminder for the requester posts to the channel, keep their own words for them", async ({ + test("when a reminder for the requester posts to the channel, save it for the creator", async ({ run, }) => { const conversation = await run( @@ -90,10 +90,11 @@ describe("Schedule Creation", () => { conversation, ); expect(creates).toHaveLength(1); - // Each run binds "me" and "my" to the creator identity, so the saved - // instruction keeps the requester's words instead of a stored mention. + // The run does not see this request, so "me" would mean nothing. Each run + // binds "the creator" to the creator identity, so no mention is saved. const { instruction } = creates[0]!.input as { instruction?: string }; - expect(instruction).toMatch(/\b(?:me|my)\b/i); + expect(instruction).toMatch(/\bcreator\b/i); + expect(instruction).not.toMatch(/\b(?:me|my)\b/i); expect(instruction).not.toContain("<@U0TEST>"); }); diff --git a/packages/junior-evals/evals/integration/scheduler/delivery.eval.ts b/packages/junior-evals/evals/integration/scheduler/delivery.eval.ts index fb608600da..58766fdf86 100644 --- a/packages/junior-evals/evals/integration/scheduler/delivery.eval.ts +++ b/packages/junior-evals/evals/integration/scheduler/delivery.eval.ts @@ -12,6 +12,7 @@ import { import { test } from "@junior-evals/fixture/test"; describe("Scheduled Delivery", () => { + // Instructions saved before "the creator" rule can still say "me". test("when a due reminder says me, mention its creator", async ({ run }) => { await insertScheduledAutomation({ credentialMode: "system", @@ -27,6 +28,23 @@ describe("Scheduled Delivery", () => { expect(delivery.replies[0]!.text).toContain("<@U0TEST>"); }); + test("when a due reminder names the creator, mention its creator", async ({ + run, + }) => { + await insertScheduledAutomation({ + credentialMode: "system", + destination: slackChannel(), + due: true, + once: true, + task: "Remind the creator to do healthchecks.", + }); + + const delivery = await run(heartbeat()); + + expect(delivery.replies).toHaveLength(1); + expect(delivery.replies[0]!.text).toContain("<@U0TEST>"); + }); + test("when a person replies to a delivered reminder, answer in its thread as a normal chat reply", async ({ run, }) => { diff --git a/packages/junior/src/chat/README.md b/packages/junior/src/chat/README.md index 1666a3710c..6e6ce6a12a 100644 --- a/packages/junior/src/chat/README.md +++ b/packages/junior/src/chat/README.md @@ -329,7 +329,7 @@ so it has no instructions of its own. `agent-invocations/result.ts` owns its | 1 | `[task]` | yes | Task header. Same for schedule, event, and subscription. | | 2 | Origin | yes | `This is a task, not a message from a person.` | | 3 | `About:` | no | One-line resource label. | -| 4 | `Created by:` | no | Creator mention made from the creator identity. Binds "me" and "my". | +| 4 | `Created by:` | no | Creator mention made from the creator identity. It is "the creator". | | 5 | `People involved:` | no | Event people mentions. Guidance says when to address them. | | 6 | `Done by:` | no | Linked person who did the Event, tied to its provider handle. | | 7 | `Instructions:` | yes | Stored task text or subscription intent. | diff --git a/packages/junior/src/chat/automations/README.md b/packages/junior/src/chat/automations/README.md index 4ca2b111fb..6f15e98d68 100644 --- a/packages/junior/src/chat/automations/README.md +++ b/packages/junior/src/chat/automations/README.md @@ -86,11 +86,14 @@ instruction or event selector switches creator credential use to system credentials. The edit read returns `ownedByViewer` so the editor can show these rules before a save. -An Automation runs later without the request that created it. The instruction -keeps "me" and "my" for the creator. Each run reads the creator identity and -renders its mention for the Destination in the `Created by:` task input line. -The `createdBy` copy does not decide this. Other people need an exact mention -in the instruction, because the request is gone at run time. The agent tools +An Automation runs later without the request that created it. So the +instruction names who and what it means, and calls its creator "the creator". +The instruction does not store a mention for the creator. Each run reads the +creator identity and renders its mention for the Destination in the +`Created by:` task input line. The `createdBy` copy does not decide this. +Instructions saved before this rule can say "me" or "my", or hold the creator +mention. The `Created by:` line treats those as the creator too. Other people +need an exact mention in the instruction. The agent tools that create or update an Automation reject an instruction that names the requester by display name without their Slack mention. A name does not notify the person. diff --git a/packages/junior/src/chat/automations/edit-schema.ts b/packages/junior/src/chat/automations/edit-schema.ts index 92f79482d5..e77eb833fc 100644 --- a/packages/junior/src/chat/automations/edit-schema.ts +++ b/packages/junior/src/chat/automations/edit-schema.ts @@ -11,14 +11,12 @@ export const automationTitleSchema = z.string().trim().min(1).max(60); export const automationInstructionSchema = z.string().trim().min(1).max(4000); /** - * Instruction input for agent tools. An Automation runs later without the - * request around it, so a reference to that request, such as "this channel", - * must become the exact target. "Me" and "my" stay as written, because each - * run binds them to the creator identity. Other people need an exact mention. + * Instruction input for agent tools. The run does not see the request, so the + * instruction names who and what it means. Each run knows "the creator". */ export const automationInstructionToolSchema = automationInstructionSchema.describe( - 'Work to perform when the Automation runs. The run does not see this conversation, so the instruction must make sense without it. Replace each reference to this conversation, such as "this channel", "here", "this thread", or "that PR", with the exact target, such as <#C123>, a link, or an ID. Keep "me" and "my" where they mean the Automation creator; each run binds them to the creator. Name every other person with their Slack mention, such as <@U123>, including a requester who is not the creator. Preserve the user\'s scope, targets, and constraints. Do not add permissions or side effects the user did not request.', + 'Work to perform when the Automation runs. The run does not see this conversation, so words like "me" or "this conversation" will mean nothing then. Write who and what you mean. Call the person who created the Automation "the creator"; each run knows who that is. Name any other person with their Slack mention, such as <@U123>. Preserve the user\'s scope, targets, and constraints. Do not add permissions or side effects the user did not request.', ); export const automationEditFieldsSchema = z.object({ diff --git a/packages/junior/src/chat/task-input.ts b/packages/junior/src/chat/task-input.ts index e21774f42f..5536b16a7a 100644 --- a/packages/junior/src/chat/task-input.ts +++ b/packages/junior/src/chat/task-input.ts @@ -72,7 +72,7 @@ export function renderTaskInput(args: { about?: string; /** * Creator mention for the Destination, rendered from the creator identity - * at run time. Stored instructions keep "me" and "my"; this binds them. + * at run time. Instructions call this person "the creator". */ creator?: string; /** Mentions of the people involved in an Event. Guidance says when to address them. */ @@ -106,11 +106,12 @@ export function renderTaskInput(args: { "This is a task, not a message from a person.", "", ...(about ? [`About: ${oneLine(about)}`] : []), - // Bind "me" to the creator, then say what to write. With only the - // binding, the model often writes "your" and drops the mention. + // Say who the creator is and what to write. With only who "me" is, the + // model often writes "your" and drops the mention. Instructions saved + // before "the creator" can still say "me" or hold a stored mention. ...(creator ? [ - `Created by: ${oneLine(creator)}. "Me" and "my" in the instructions mean this person. Where the instructions say "me" or "my", write this mention. If they do not say "me" or "my", do not mention this person.`, + `Created by: ${oneLine(creator)}. This person is the creator. Where the instructions refer to the creator, write this mention. "Me", "my", and this mention in the instructions also mean the creator. If the instructions do not refer to the creator, do not mention this person.`, ] : []), ...(people.length > 0 ? [`People involved: ${people.join(", ")}`] : []), diff --git a/packages/junior/tests/integration/event-automations.test.ts b/packages/junior/tests/integration/event-automations.test.ts index a6e11a0fc7..d87080914e 100644 --- a/packages/junior/tests/integration/event-automations.test.ts +++ b/packages/junior/tests/integration/event-automations.test.ts @@ -406,7 +406,7 @@ describe("event automations", () => { This is a task, not a message from a person. About: GitHub PR getsentry/junior#1174 - Created by: <@U123>. "Me" and "my" in the instructions mean this person. Where the instructions say "me" or "my", write this mention. If they do not say "me" or "my", do not mention this person. + Created by: <@U123>. This person is the creator. Where the instructions refer to the creator, write this mention. "Me", "my", and this mention in the instructions also mean the creator. If the instructions do not refer to the creator, do not mention this person. Instructions: Address the requested changes. Trusted summary: A reviewer requested changes. diff --git a/packages/junior/tests/unit/chat/task-input.test.ts b/packages/junior/tests/unit/chat/task-input.test.ts index a1ddcce016..a25fd272ca 100644 --- a/packages/junior/tests/unit/chat/task-input.test.ts +++ b/packages/junior/tests/unit/chat/task-input.test.ts @@ -36,7 +36,7 @@ describe("renderTaskInput", () => { This is a task, not a message from a person. About: GitHub PR getsentry/junior#691 - Created by: <@U123>. "Me" and "my" in the instructions mean this person. Where the instructions say "me" or "my", write this mention. If they do not say "me" or "my", do not mention this person. + Created by: <@U123>. This person is the creator. Where the instructions refer to the creator, write this mention. "Me", "my", and this mention in the instructions also mean the creator. If the instructions do not refer to the creator, do not mention this person. People involved: <@U456> Done by: <@U456>. This person did what this Event describes. The Event calls them "octocat". That is the same person. They are also in People involved. Write to them as "you", and do not also call them "octocat". Instructions: Fix failed checks on this PR. From b0eeaa6d3647f7422c3b1cde5b03074a0f78e32a Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:06:46 +0000 Subject: [PATCH 6/8] fix(automations): Use "the Automation creator" and keep exact targets "The creator" also matched resource creators, such as an issue creator. Use the full term in the tool description and the Created by line. Bring back the rule to write exact targets instead of references to the conversation. Reject "me" and "my" in an update by a requester who did not create the Automation, because each run reads them as the creator. --- .../src/mock-reporting/fixtures.ts | 2 +- .../integration/scheduler/creation.eval.ts | 8 +++-- .../integration/scheduler/delivery.eval.ts | 6 ++-- packages/junior/src/chat/README.md | 28 ++++++++--------- .../junior/src/chat/automations/README.md | 22 +++++++------- .../src/chat/automations/edit-schema.ts | 5 ++-- .../src/chat/automations/requester-mention.ts | 16 +++++++--- .../scheduled-automations/tools/create.ts | 4 ++- .../scheduled-automations/tools/update.ts | 4 ++- packages/junior/src/chat/task-input.ts | 9 +++--- .../src/chat/tools/create-event-automation.ts | 2 +- .../src/chat/tools/update-event-automation.ts | 2 +- .../integration/event-automations.test.ts | 2 +- .../tests/unit/chat/requester-mention.test.ts | 30 +++++++++++++++++-- .../junior/tests/unit/chat/task-input.test.ts | 2 +- 15 files changed, 93 insertions(+), 49 deletions(-) diff --git a/packages/junior-dashboard/src/mock-reporting/fixtures.ts b/packages/junior-dashboard/src/mock-reporting/fixtures.ts index ae7f7cdd2f..cb3651f982 100644 --- a/packages/junior-dashboard/src/mock-reporting/fixtures.ts +++ b/packages/junior-dashboard/src/mock-reporting/fixtures.ts @@ -1451,7 +1451,7 @@ function eventAutomationRunConversation( "This is a task, not a message from a person.", "", "About: Issue · ACME-42", - 'Created by: <@U123>. This person is the creator. Where the instructions refer to the creator, write this mention. "Me", "my", and this mention in the instructions also mean the creator. If the instructions do not refer to the creator, do not mention this person.', + 'Created by: <@U123>. This person is the Automation creator. Where the instructions say "the Automation creator", write this mention. "Me", "my", and this mention in the instructions also mean the Automation creator. If the instructions do not refer to the Automation creator, do not mention this person.', "Instructions: Summarize the closed issue", "", "Trusted summary: ACME-42 Checkout fails for saved cards was closed by Morgan Lee.", diff --git a/packages/junior-evals/evals/integration/scheduler/creation.eval.ts b/packages/junior-evals/evals/integration/scheduler/creation.eval.ts index 6c0a8d0b15..136853714b 100644 --- a/packages/junior-evals/evals/integration/scheduler/creation.eval.ts +++ b/packages/junior-evals/evals/integration/scheduler/creation.eval.ts @@ -76,7 +76,7 @@ describe("Schedule Creation", () => { expect(creates[0]!.input).not.toHaveProperty("nextRunAt"); }); - test("when a reminder for the requester posts to the channel, save it for the creator", async ({ + test("when a reminder for the requester posts to the channel, save it for the Automation creator without conversation references", async ({ run, }) => { const conversation = await run( @@ -91,11 +91,13 @@ describe("Schedule Creation", () => { ); expect(creates).toHaveLength(1); // The run does not see this request, so "me" would mean nothing. Each run - // binds "the creator" to the creator identity, so no mention is saved. + // binds "the Automation creator" to the creator identity, so no mention is + // saved. "This channel" would also mean nothing, and nothing binds it. const { instruction } = creates[0]!.input as { instruction?: string }; - expect(instruction).toMatch(/\bcreator\b/i); + expect(instruction).toMatch(/\bAutomation creator\b/i); expect(instruction).not.toMatch(/\b(?:me|my)\b/i); expect(instruction).not.toContain("<@U0TEST>"); + expect(instruction).not.toMatch(/\bthis channel\b/i); }); test("when asked to tell the channel something later, preserve the future work in the schedule", async ({ diff --git a/packages/junior-evals/evals/integration/scheduler/delivery.eval.ts b/packages/junior-evals/evals/integration/scheduler/delivery.eval.ts index 58766fdf86..6fd394fefc 100644 --- a/packages/junior-evals/evals/integration/scheduler/delivery.eval.ts +++ b/packages/junior-evals/evals/integration/scheduler/delivery.eval.ts @@ -12,7 +12,7 @@ import { import { test } from "@junior-evals/fixture/test"; describe("Scheduled Delivery", () => { - // Instructions saved before "the creator" rule can still say "me". + // Instructions saved before "the Automation creator" can still say "me". test("when a due reminder says me, mention its creator", async ({ run }) => { await insertScheduledAutomation({ credentialMode: "system", @@ -28,7 +28,7 @@ describe("Scheduled Delivery", () => { expect(delivery.replies[0]!.text).toContain("<@U0TEST>"); }); - test("when a due reminder names the creator, mention its creator", async ({ + test("when a due reminder names the Automation creator, mention its creator", async ({ run, }) => { await insertScheduledAutomation({ @@ -36,7 +36,7 @@ describe("Scheduled Delivery", () => { destination: slackChannel(), due: true, once: true, - task: "Remind the creator to do healthchecks.", + task: "Remind the Automation creator to do healthchecks.", }); const delivery = await run(heartbeat()); diff --git a/packages/junior/src/chat/README.md b/packages/junior/src/chat/README.md index 6e6ce6a12a..ca1cf1efc9 100644 --- a/packages/junior/src/chat/README.md +++ b/packages/junior/src/chat/README.md @@ -324,20 +324,20 @@ so it has no instructions of its own. `agent-invocations/result.ts` owns its **Section order** (omit empty optionals) -| # | Section | Required | Role | -| --- | ------------------- | -------- | -------------------------------------------------------------------- | -| 1 | `[task]` | yes | Task header. Same for schedule, event, and subscription. | -| 2 | Origin | yes | `This is a task, not a message from a person.` | -| 3 | `About:` | no | One-line resource label. | -| 4 | `Created by:` | no | Creator mention made from the creator identity. It is "the creator". | -| 5 | `People involved:` | no | Event people mentions. Guidance says when to address them. | -| 6 | `Done by:` | no | Linked person who did the Event, tied to its provider handle. | -| 7 | `Instructions:` | yes | Stored task text or subscription intent. | -| 8 | Additional guidance | no | Under instructions; cannot replace them or grant authority. | -| 9 | `Trusted summary:` | no | Optional trusted one-line summary. | -| 10 | Verified details | no | Trusted structured fields as JSON. | -| 11 | External text | no | Untrusted provider text; information only. | -| 12 | Outcome | yes | Stored outcome rule. Always last. | +| # | Section | Required | Role | +| --- | ------------------- | -------- | ---------------------------------------------------------------- | +| 1 | `[task]` | yes | Task header. Same for schedule, event, and subscription. | +| 2 | Origin | yes | `This is a task, not a message from a person.` | +| 3 | `About:` | no | One-line resource label. | +| 4 | `Created by:` | no | Creator mention from the identity, for "the Automation creator". | +| 5 | `People involved:` | no | Event people mentions. Guidance says when to address them. | +| 6 | `Done by:` | no | Linked person who did the Event, tied to its provider handle. | +| 7 | `Instructions:` | yes | Stored task text or subscription intent. | +| 8 | Additional guidance | no | Under instructions; cannot replace them or grant authority. | +| 9 | `Trusted summary:` | no | Optional trusted one-line summary. | +| 10 | Verified details | no | Trusted structured fields as JSON. | +| 11 | External text | no | Untrusted provider text; information only. | +| 12 | Outcome | yes | Stored outcome rule. Always last. | **Watch outcome** (exact lines; no `outcomes` passed) diff --git a/packages/junior/src/chat/automations/README.md b/packages/junior/src/chat/automations/README.md index 6f15e98d68..1a476dcbf3 100644 --- a/packages/junior/src/chat/automations/README.md +++ b/packages/junior/src/chat/automations/README.md @@ -87,16 +87,18 @@ credentials. The edit read returns `ownedByViewer` so the editor can show these rules before a save. An Automation runs later without the request that created it. So the -instruction names who and what it means, and calls its creator "the creator". -The instruction does not store a mention for the creator. Each run reads the -creator identity and renders its mention for the Destination in the -`Created by:` task input line. The `createdBy` copy does not decide this. -Instructions saved before this rule can say "me" or "my", or hold the creator -mention. The `Created by:` line treats those as the creator too. Other people -need an exact mention in the instruction. The agent tools -that create or update an Automation reject an instruction that names the -requester by display name without their Slack mention. A name does not notify -the person. +instruction names exact targets, such as a channel mention or a link. It calls +its creator "the Automation creator" and does not store a mention for them. +Each run reads the creator identity and renders its mention for the +Destination in the `Created by:` task input line. The `createdBy` copy does not +decide this. For compatibility, that line also treats "me", "my", and a stored +creator mention as the Automation creator, because older instructions use them. + +Other people need an exact mention in the instruction. The agent tools that +create or update an Automation reject an instruction that names the requester +by display name without their Slack mention. A name does not notify the +person. An update by a requester who did not create the Automation also +rejects "me" and "my", because each run reads them as the Automation creator. ## Versions diff --git a/packages/junior/src/chat/automations/edit-schema.ts b/packages/junior/src/chat/automations/edit-schema.ts index e77eb833fc..74679ff28d 100644 --- a/packages/junior/src/chat/automations/edit-schema.ts +++ b/packages/junior/src/chat/automations/edit-schema.ts @@ -12,11 +12,12 @@ export const automationInstructionSchema = z.string().trim().min(1).max(4000); /** * Instruction input for agent tools. The run does not see the request, so the - * instruction names who and what it means. Each run knows "the creator". + * instruction names exact targets. Each run knows "the Automation creator"; + * every other person needs their mention. */ export const automationInstructionToolSchema = automationInstructionSchema.describe( - 'Work to perform when the Automation runs. The run does not see this conversation, so words like "me" or "this conversation" will mean nothing then. Write who and what you mean. Call the person who created the Automation "the creator"; each run knows who that is. Name any other person with their Slack mention, such as <@U123>. Preserve the user\'s scope, targets, and constraints. Do not add permissions or side effects the user did not request.', + 'Work to perform when the Automation runs. The run does not see this conversation, so words like "me" or "this channel" will mean nothing then. Write the exact target instead, such as a channel mention or a link. Call the person who created the Automation "the Automation creator"; each run knows who that is. Name every other person with their Slack mention, such as <@U123>, including a requester who did not create the Automation. Preserve the user\'s scope, targets, and constraints. Do not add permissions or side effects the user did not request.', ); export const automationEditFieldsSchema = z.object({ diff --git a/packages/junior/src/chat/automations/requester-mention.ts b/packages/junior/src/chat/automations/requester-mention.ts index fe59e6a7ef..31b9875bd5 100644 --- a/packages/junior/src/chat/automations/requester-mention.ts +++ b/packages/junior/src/chat/automations/requester-mention.ts @@ -11,18 +11,26 @@ function namesPerson(text: string, name: string): boolean { } /** - * Reject an instruction that names the requester without their Slack mention. + * Reject an instruction that does not name the requester exactly. * * An Automation runs later without the request. A display name does not - * notify the person, and it can match another person. + * notify the person, and it can match another person. Each run reads "me" and + * "my" as the Automation creator, so a requester who did not create the + * Automation must use their mention instead. */ export function requireRequesterMention( instruction: string, actor: Actor | undefined, + requester: { isCreator: boolean }, ): void { - if (actor?.platform !== "slack" || !actor.fullName) return; + if (actor?.platform !== "slack") return; const mention = slackMention(actor.userId); - if (instruction.includes(mention)) return; + if (!requester.isCreator && /\b(?:me|my)\b/i.test(instruction)) { + throw new ToolInputError( + `The requester did not create this Automation, and each run reads "me" and "my" as the Automation creator. Write ${mention} for the requester.`, + ); + } + if (!actor.fullName || instruction.includes(mention)) return; if (namesPerson(instruction, actor.fullName)) { throw new ToolInputError( `The instruction names ${actor.fullName} without a Slack mention. Write ${mention} for this person.`, diff --git a/packages/junior/src/chat/scheduled-automations/tools/create.ts b/packages/junior/src/chat/scheduled-automations/tools/create.ts index 7fe8947656..41977b07a8 100644 --- a/packages/junior/src/chat/scheduled-automations/tools/create.ts +++ b/packages/junior/src/chat/scheduled-automations/tools/create.ts @@ -131,7 +131,9 @@ export function createSlackScheduleCreateAutomationTool( ); } - requireRequesterMention(input.instruction, context.actor); + requireRequesterMention(input.instruction, context.actor, { + isCreator: true, + }); const creator = await context.users.resolveActor(); const identity = creator?.identity; if ( diff --git a/packages/junior/src/chat/scheduled-automations/tools/update.ts b/packages/junior/src/chat/scheduled-automations/tools/update.ts index 8367f4ec00..1d32aa1a18 100644 --- a/packages/junior/src/chat/scheduled-automations/tools/update.ts +++ b/packages/junior/src/chat/scheduled-automations/tools/update.ts @@ -164,7 +164,9 @@ export function createSlackScheduleUpdateAutomationTool( input.instruction !== undefined && input.instruction !== lookup.task.text; if (input.instruction !== undefined && instructionChanged) { - requireRequesterMention(input.instruction, context.actor); + requireRequesterMention(input.instruction, context.actor, { + isCreator, + }); } const next = await editScheduledAutomation( lookup, diff --git a/packages/junior/src/chat/task-input.ts b/packages/junior/src/chat/task-input.ts index 5536b16a7a..316a983d07 100644 --- a/packages/junior/src/chat/task-input.ts +++ b/packages/junior/src/chat/task-input.ts @@ -72,7 +72,7 @@ export function renderTaskInput(args: { about?: string; /** * Creator mention for the Destination, rendered from the creator identity - * at run time. Instructions call this person "the creator". + * at run time. Instructions call this person "the Automation creator". */ creator?: string; /** Mentions of the people involved in an Event. Guidance says when to address them. */ @@ -107,11 +107,12 @@ export function renderTaskInput(args: { "", ...(about ? [`About: ${oneLine(about)}`] : []), // Say who the creator is and what to write. With only who "me" is, the - // model often writes "your" and drops the mention. Instructions saved - // before "the creator" can still say "me" or hold a stored mention. + // model often writes "your" and drops the mention. The full term keeps + // "issue creator" or "PR creator" from matching. Compatibility: older + // saved instructions can say "me" or "my", or hold the creator mention. ...(creator ? [ - `Created by: ${oneLine(creator)}. This person is the creator. Where the instructions refer to the creator, write this mention. "Me", "my", and this mention in the instructions also mean the creator. If the instructions do not refer to the creator, do not mention this person.`, + `Created by: ${oneLine(creator)}. This person is the Automation creator. Where the instructions say "the Automation creator", write this mention. "Me", "my", and this mention in the instructions also mean the Automation creator. If the instructions do not refer to the Automation creator, do not mention this person.`, ] : []), ...(people.length > 0 ? [`People involved: ${people.join(", ")}`] : []), diff --git a/packages/junior/src/chat/tools/create-event-automation.ts b/packages/junior/src/chat/tools/create-event-automation.ts index 5eb1df8f5c..9fc73df20b 100644 --- a/packages/junior/src/chat/tools/create-event-automation.ts +++ b/packages/junior/src/chat/tools/create-event-automation.ts @@ -128,7 +128,7 @@ export function createEventAutomationTool( async execute(input, options) { const { actor, destination, source } = requireEventAutomationSlackContext(context); - requireRequesterMention(input.instruction, actor); + requireRequesterMention(input.instruction, actor, { isCreator: true }); const trigger = requireEventAutomationTrigger(catalog, input.trigger); const id = buildEventAutomationId({ channelId: destination.channelId, diff --git a/packages/junior/src/chat/tools/update-event-automation.ts b/packages/junior/src/chat/tools/update-event-automation.ts index 58c540e2f5..4bc9b69931 100644 --- a/packages/junior/src/chat/tools/update-event-automation.ts +++ b/packages/junior/src/chat/tools/update-event-automation.ts @@ -130,7 +130,7 @@ export function createUpdateEventAutomationTool( input.instruction != null && input.instruction !== current.task.text ) { - requireRequesterMention(input.instruction, actor); + requireRequesterMention(input.instruction, actor, { isCreator }); } let saved = current; if (edits) { diff --git a/packages/junior/tests/integration/event-automations.test.ts b/packages/junior/tests/integration/event-automations.test.ts index d87080914e..cfe49ee793 100644 --- a/packages/junior/tests/integration/event-automations.test.ts +++ b/packages/junior/tests/integration/event-automations.test.ts @@ -406,7 +406,7 @@ describe("event automations", () => { This is a task, not a message from a person. About: GitHub PR getsentry/junior#1174 - Created by: <@U123>. This person is the creator. Where the instructions refer to the creator, write this mention. "Me", "my", and this mention in the instructions also mean the creator. If the instructions do not refer to the creator, do not mention this person. + Created by: <@U123>. This person is the Automation creator. Where the instructions say "the Automation creator", write this mention. "Me", "my", and this mention in the instructions also mean the Automation creator. If the instructions do not refer to the Automation creator, do not mention this person. Instructions: Address the requested changes. Trusted summary: A reviewer requested changes. diff --git a/packages/junior/tests/unit/chat/requester-mention.test.ts b/packages/junior/tests/unit/chat/requester-mention.test.ts index 8a6b64db4f..a72451d744 100644 --- a/packages/junior/tests/unit/chat/requester-mention.test.ts +++ b/packages/junior/tests/unit/chat/requester-mention.test.ts @@ -12,7 +12,11 @@ const actor = { describe("requireRequesterMention", () => { it("rejects the requester's name when the mention is missing", () => { expect(() => - requireRequesterMention("Remind sam lee to submit the timesheet.", actor), + requireRequesterMention( + "Remind sam lee to submit the timesheet.", + actor, + { isCreator: true }, + ), ).toThrow( new ToolInputError( "The instruction names Sam Lee without a Slack mention. Write <@U0TEST> for this person.", @@ -27,8 +31,30 @@ describe("requireRequesterMention", () => { "Post the weekly digest.", // The name is part of another word. "Ask Sam Leeds for the report.", + // Older instructions and the creator's own edits can still say "me". + "Remind me to submit my timesheet.", ]) { - expect(() => requireRequesterMention(instruction, actor)).not.toThrow(); + expect(() => + requireRequesterMention(instruction, actor, { isCreator: true }), + ).not.toThrow(); } }); + + it('rejects "me" from a requester who did not create the Automation', () => { + // Each run reads "me" as the Automation creator, a different person. + expect(() => + requireRequesterMention("Remind me to review the PR.", actor, { + isCreator: false, + }), + ).toThrow( + new ToolInputError( + 'The requester did not create this Automation, and each run reads "me" and "my" as the Automation creator. Write <@U0TEST> for the requester.', + ), + ); + expect(() => + requireRequesterMention("Remind <@U0TEST> to review the PR.", actor, { + isCreator: false, + }), + ).not.toThrow(); + }); }); diff --git a/packages/junior/tests/unit/chat/task-input.test.ts b/packages/junior/tests/unit/chat/task-input.test.ts index a25fd272ca..85310237cf 100644 --- a/packages/junior/tests/unit/chat/task-input.test.ts +++ b/packages/junior/tests/unit/chat/task-input.test.ts @@ -36,7 +36,7 @@ describe("renderTaskInput", () => { This is a task, not a message from a person. About: GitHub PR getsentry/junior#691 - Created by: <@U123>. This person is the creator. Where the instructions refer to the creator, write this mention. "Me", "my", and this mention in the instructions also mean the creator. If the instructions do not refer to the creator, do not mention this person. + Created by: <@U123>. This person is the Automation creator. Where the instructions say "the Automation creator", write this mention. "Me", "my", and this mention in the instructions also mean the Automation creator. If the instructions do not refer to the Automation creator, do not mention this person. People involved: <@U456> Done by: <@U456>. This person did what this Event describes. The Event calls them "octocat". That is the same person. They are also in People involved. Write to them as "you", and do not also call them "octocat". Instructions: Fix failed checks on this PR. From a8eeb4479cdcec172928b60ee7c14360012a47bc Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:17:08 +0000 Subject: [PATCH 7/8] fix(automations): Drop the creator lookup and the me/my check Creation already ties the creator identity to the Slack actor, so the lookup could not change the mention. Keep createdBy for the Created by: line. Replace the hard "me"/"my" rejection with tool guidance and an eval for a requester who did not create the Automation. Simplify the Created by: line to one rule. Co-Authored-By: David Cramer --- .../src/mock-reporting/fixtures.ts | 2 +- .../integration/scheduler/creation.eval.ts | 5 +-- .../integration/scheduler/delivery.eval.ts | 1 - .../integration/scheduler/management.eval.ts | 30 ++++++++++++++++ packages/junior/src/chat/README.md | 28 +++++++-------- .../junior/src/chat/automations/README.md | 24 ++++++------- .../junior/src/chat/automations/creator.ts | 34 ------------------- .../src/chat/automations/edit-schema.ts | 6 ++-- .../src/chat/automations/requester-mention.ts | 16 +++------ .../src/chat/event-automations/ingest.ts | 13 ++----- .../chat/scheduled-automations/heartbeat.ts | 11 +++--- .../scheduled-automations/tools/create.ts | 4 +-- .../scheduled-automations/tools/update.ts | 4 +-- packages/junior/src/chat/task-input.ts | 12 ++----- .../src/chat/tools/create-event-automation.ts | 2 +- .../src/chat/tools/update-event-automation.ts | 2 +- .../integration/event-automations.test.ts | 2 +- .../heartbeat-scheduled-automations.test.ts | 4 +-- .../tests/unit/chat/requester-mention.test.ts | 30 ++-------------- .../junior/tests/unit/chat/task-input.test.ts | 2 +- 20 files changed, 83 insertions(+), 149 deletions(-) diff --git a/packages/junior-dashboard/src/mock-reporting/fixtures.ts b/packages/junior-dashboard/src/mock-reporting/fixtures.ts index cb3651f982..e6c2a4f19c 100644 --- a/packages/junior-dashboard/src/mock-reporting/fixtures.ts +++ b/packages/junior-dashboard/src/mock-reporting/fixtures.ts @@ -1451,7 +1451,7 @@ function eventAutomationRunConversation( "This is a task, not a message from a person.", "", "About: Issue · ACME-42", - 'Created by: <@U123>. This person is the Automation creator. Where the instructions say "the Automation creator", write this mention. "Me", "my", and this mention in the instructions also mean the Automation creator. If the instructions do not refer to the Automation creator, do not mention this person.', + 'Created by: <@U123>. This person is the Automation creator. "The Automation creator", "me", and "my" in the instructions mean this person; write this mention for them. If the instructions do not refer to this person, do not mention them.', "Instructions: Summarize the closed issue", "", "Trusted summary: ACME-42 Checkout fails for saved cards was closed by Morgan Lee.", diff --git a/packages/junior-evals/evals/integration/scheduler/creation.eval.ts b/packages/junior-evals/evals/integration/scheduler/creation.eval.ts index 136853714b..fbf1260d7b 100644 --- a/packages/junior-evals/evals/integration/scheduler/creation.eval.ts +++ b/packages/junior-evals/evals/integration/scheduler/creation.eval.ts @@ -90,13 +90,10 @@ describe("Schedule Creation", () => { conversation, ); expect(creates).toHaveLength(1); - // The run does not see this request, so "me" would mean nothing. Each run - // binds "the Automation creator" to the creator identity, so no mention is - // saved. "This channel" would also mean nothing, and nothing binds it. + // The run does not see this request, so "this channel" would mean nothing. const { instruction } = creates[0]!.input as { instruction?: string }; expect(instruction).toMatch(/\bAutomation creator\b/i); expect(instruction).not.toMatch(/\b(?:me|my)\b/i); - expect(instruction).not.toContain("<@U0TEST>"); expect(instruction).not.toMatch(/\bthis channel\b/i); }); diff --git a/packages/junior-evals/evals/integration/scheduler/delivery.eval.ts b/packages/junior-evals/evals/integration/scheduler/delivery.eval.ts index 6fd394fefc..7d268ff61d 100644 --- a/packages/junior-evals/evals/integration/scheduler/delivery.eval.ts +++ b/packages/junior-evals/evals/integration/scheduler/delivery.eval.ts @@ -12,7 +12,6 @@ import { import { test } from "@junior-evals/fixture/test"; describe("Scheduled Delivery", () => { - // Instructions saved before "the Automation creator" can still say "me". test("when a due reminder says me, mention its creator", async ({ run }) => { await insertScheduledAutomation({ credentialMode: "system", diff --git a/packages/junior-evals/evals/integration/scheduler/management.eval.ts b/packages/junior-evals/evals/integration/scheduler/management.eval.ts index 1e4abf0e6e..b83f61a022 100644 --- a/packages/junior-evals/evals/integration/scheduler/management.eval.ts +++ b/packages/junior-evals/evals/integration/scheduler/management.eval.ts @@ -76,4 +76,34 @@ describe("Schedule Management", () => { expect(update.input).not.toHaveProperty("nextRunAt"); } }); + + test("when a requester who did not create a reminder adds themselves, name them with their mention", async ({ + run, + }) => { + const channel = slackChannel(); + await insertScheduledAutomation({ + createdBy: alice, + credentialMode: "system", + destination: channel, + task: "Post a planning reminder in this channel.", + }); + + const conversation = await run( + slackMention( + "change the scheduled planning reminder now so it also reminds me to bring the agenda.", + { channel }, + ), + ); + + const updates = completedToolCalls( + "slackScheduleUpdateAutomation", + conversation, + ); + expect(updates).toHaveLength(1); + // Each run reads "me" and "the Automation creator" as Alice. + const { instruction } = updates[0]!.input as { instruction?: string }; + expect(instruction).toContain("<@U0TEST>"); + expect(instruction).not.toMatch(/\b(?:me|my)\b/i); + expect(instruction).not.toMatch(/\bAutomation creator\b/i); + }); }); diff --git a/packages/junior/src/chat/README.md b/packages/junior/src/chat/README.md index ca1cf1efc9..d8066c3204 100644 --- a/packages/junior/src/chat/README.md +++ b/packages/junior/src/chat/README.md @@ -324,20 +324,20 @@ so it has no instructions of its own. `agent-invocations/result.ts` owns its **Section order** (omit empty optionals) -| # | Section | Required | Role | -| --- | ------------------- | -------- | ---------------------------------------------------------------- | -| 1 | `[task]` | yes | Task header. Same for schedule, event, and subscription. | -| 2 | Origin | yes | `This is a task, not a message from a person.` | -| 3 | `About:` | no | One-line resource label. | -| 4 | `Created by:` | no | Creator mention from the identity, for "the Automation creator". | -| 5 | `People involved:` | no | Event people mentions. Guidance says when to address them. | -| 6 | `Done by:` | no | Linked person who did the Event, tied to its provider handle. | -| 7 | `Instructions:` | yes | Stored task text or subscription intent. | -| 8 | Additional guidance | no | Under instructions; cannot replace them or grant authority. | -| 9 | `Trusted summary:` | no | Optional trusted one-line summary. | -| 10 | Verified details | no | Trusted structured fields as JSON. | -| 11 | External text | no | Untrusted provider text; information only. | -| 12 | Outcome | yes | Stored outcome rule. Always last. | +| # | Section | Required | Role | +| --- | ------------------- | -------- | -------------------------------------------------------------------- | +| 1 | `[task]` | yes | Task header. Same for schedule, event, and subscription. | +| 2 | Origin | yes | `This is a task, not a message from a person.` | +| 3 | `About:` | no | One-line resource label. | +| 4 | `Created by:` | no | Creator mention. "The Automation creator", "me", and "my" mean them. | +| 5 | `People involved:` | no | Event people mentions. Guidance says when to address them. | +| 6 | `Done by:` | no | Linked person who did the Event, tied to its provider handle. | +| 7 | `Instructions:` | yes | Stored task text or subscription intent. | +| 8 | Additional guidance | no | Under instructions; cannot replace them or grant authority. | +| 9 | `Trusted summary:` | no | Optional trusted one-line summary. | +| 10 | Verified details | no | Trusted structured fields as JSON. | +| 11 | External text | no | Untrusted provider text; information only. | +| 12 | Outcome | yes | Stored outcome rule. Always last. | **Watch outcome** (exact lines; no `outcomes` passed) diff --git a/packages/junior/src/chat/automations/README.md b/packages/junior/src/chat/automations/README.md index 1a476dcbf3..6eba63bbd1 100644 --- a/packages/junior/src/chat/automations/README.md +++ b/packages/junior/src/chat/automations/README.md @@ -86,19 +86,17 @@ instruction or event selector switches creator credential use to system credentials. The edit read returns `ownedByViewer` so the editor can show these rules before a save. -An Automation runs later without the request that created it. So the -instruction names exact targets, such as a channel mention or a link. It calls -its creator "the Automation creator" and does not store a mention for them. -Each run reads the creator identity and renders its mention for the -Destination in the `Created by:` task input line. The `createdBy` copy does not -decide this. For compatibility, that line also treats "me", "my", and a stored -creator mention as the Automation creator, because older instructions use them. - -Other people need an exact mention in the instruction. The agent tools that -create or update an Automation reject an instruction that names the requester -by display name without their Slack mention. A name does not notify the -person. An update by a requester who did not create the Automation also -rejects "me" and "my", because each run reads them as the Automation creator. +An Automation runs later without the request that created it. So the agent +tools tell the agent to write exact targets, such as a channel mention or a +link. The agent calls the creator "the Automation creator". It names every +other person with their Slack mention, also a requester who did not create the +Automation. Each run puts the creator mention in the `Created by:` task input +line. That line also says that "me" and "my" mean the creator. People can +write these words in the dashboard editor. + +The agent tools that create or update an Automation reject an instruction that +names the requester by display name without their Slack mention. A name does +not notify the person. ## Versions diff --git a/packages/junior/src/chat/automations/creator.ts b/packages/junior/src/chat/automations/creator.ts index fcc0cdabfa..7a041b26ae 100644 --- a/packages/junior/src/chat/automations/creator.ts +++ b/packages/junior/src/chat/automations/creator.ts @@ -1,7 +1,4 @@ import type { User } from "@sentry/junior-plugin-api"; -import { readIdentityByIdFromSql } from "@/chat/plugins/viewer"; -import { slackMention } from "@/chat/slack/mrkdwn"; -import type { JuniorDatabase } from "@/db/db"; /** * Return whether the user created the Automation. Both kinds store the @@ -15,34 +12,3 @@ export function isAutomationCreator( (identity) => identity.id === automation.creatorIdentityId, ); } - -/** - * Render the Automation creator for a run in its Destination. - * - * The creator identity is the source of truth, not the `createdBy` copy. The - * create tools store the creator's Slack identity in the Destination - * workspace, and the row cannot be deleted while the Automation exists. A - * creator that does not resolve there is a broken invariant, so the run fails - * instead of binding "me" to nobody or to the wrong person. - */ -export async function automationCreatorMention( - db: JuniorDatabase, - automation: { - id: string; - creatorIdentityId: string; - destination: { teamId: string }; - }, -): Promise { - const identity = ( - await readIdentityByIdFromSql(db, automation.creatorIdentityId) - )?.identity; - if ( - identity?.provider !== "slack" || - identity.providerTenantId !== automation.destination.teamId - ) { - throw new Error( - `Automation ${automation.id} creator identity does not resolve in its Destination workspace.`, - ); - } - return slackMention(identity.providerSubjectId); -} diff --git a/packages/junior/src/chat/automations/edit-schema.ts b/packages/junior/src/chat/automations/edit-schema.ts index 74679ff28d..74f4de0ce7 100644 --- a/packages/junior/src/chat/automations/edit-schema.ts +++ b/packages/junior/src/chat/automations/edit-schema.ts @@ -12,12 +12,12 @@ export const automationInstructionSchema = z.string().trim().min(1).max(4000); /** * Instruction input for agent tools. The run does not see the request, so the - * instruction names exact targets. Each run knows "the Automation creator"; - * every other person needs their mention. + * instruction names exact targets. It calls the creator "the Automation + * creator". It names every other person with their mention. */ export const automationInstructionToolSchema = automationInstructionSchema.describe( - 'Work to perform when the Automation runs. The run does not see this conversation, so words like "me" or "this channel" will mean nothing then. Write the exact target instead, such as a channel mention or a link. Call the person who created the Automation "the Automation creator"; each run knows who that is. Name every other person with their Slack mention, such as <@U123>, including a requester who did not create the Automation. Preserve the user\'s scope, targets, and constraints. Do not add permissions or side effects the user did not request.', + 'Work to perform when the Automation runs. The run does not see this conversation, so words like "this channel" or "here" will mean nothing then. Write the exact target instead, such as a channel mention or a link. Call the person who created the Automation "the Automation creator", not "me". Name every other person with their Slack mention, such as <@U123>, including a requester who did not create the Automation. Preserve the user\'s scope, targets, and constraints. Do not add permissions or side effects the user did not request.', ); export const automationEditFieldsSchema = z.object({ diff --git a/packages/junior/src/chat/automations/requester-mention.ts b/packages/junior/src/chat/automations/requester-mention.ts index 31b9875bd5..fe59e6a7ef 100644 --- a/packages/junior/src/chat/automations/requester-mention.ts +++ b/packages/junior/src/chat/automations/requester-mention.ts @@ -11,26 +11,18 @@ function namesPerson(text: string, name: string): boolean { } /** - * Reject an instruction that does not name the requester exactly. + * Reject an instruction that names the requester without their Slack mention. * * An Automation runs later without the request. A display name does not - * notify the person, and it can match another person. Each run reads "me" and - * "my" as the Automation creator, so a requester who did not create the - * Automation must use their mention instead. + * notify the person, and it can match another person. */ export function requireRequesterMention( instruction: string, actor: Actor | undefined, - requester: { isCreator: boolean }, ): void { - if (actor?.platform !== "slack") return; + if (actor?.platform !== "slack" || !actor.fullName) return; const mention = slackMention(actor.userId); - if (!requester.isCreator && /\b(?:me|my)\b/i.test(instruction)) { - throw new ToolInputError( - `The requester did not create this Automation, and each run reads "me" and "my" as the Automation creator. Write ${mention} for the requester.`, - ); - } - if (!actor.fullName || instruction.includes(mention)) return; + if (instruction.includes(mention)) return; if (namesPerson(instruction, actor.fullName)) { throw new ToolInputError( `The instruction names ${actor.fullName} without a Slack mention. Write ${mention} for this person.`, diff --git a/packages/junior/src/chat/event-automations/ingest.ts b/packages/junior/src/chat/event-automations/ingest.ts index 02c7c94c82..f284e6a317 100644 --- a/packages/junior/src/chat/event-automations/ingest.ts +++ b/packages/junior/src/chat/event-automations/ingest.ts @@ -4,6 +4,7 @@ * Each matching automation is independently idempotent. Aggregate failures propagate * so the provider can retry the original delivery. */ +import { slackMention } from "@/chat/slack/mrkdwn"; import { createHash } from "node:crypto"; import { EVENT_SUMMARY_MAX_LENGTH, @@ -13,7 +14,6 @@ import { type TaskOutcome, } from "@sentry/junior-plugin-api"; import { dispatchEventAutomation } from "@/chat/agent-dispatch/context"; -import { automationCreatorMention } from "@/chat/automations/creator"; import { automationReplyAttribution } from "@/chat/automations/reply-attribution"; import { renderTaskInput } from "@/chat/task-input"; import { effectiveTaskOutcomes } from "@/chat/task-outcomes"; @@ -55,7 +55,6 @@ function eventAutomationDispatchKey( /** Render plain agent input for one matching event automation. */ function eventInput( task: EventAutomation, - creator: string, event: Event, outcomes: TaskOutcome[], catalog: EventCatalog, @@ -63,7 +62,7 @@ function eventInput( const guidance = eventGuidance(catalog, event.namespace, event.eventType); return renderTaskInput({ about: task.trigger.label, - creator, + creator: slackMention(task.createdBy.slackUserId), instructions: task.task.text, outcomes, guidance, @@ -169,13 +168,7 @@ export async function ingestEventAutomations( ...(credentialSubject ? { credentialSubject } : undefined), destination, destinationVisibility: task.destinationVisibility, - input: eventInput( - task, - await automationCreatorMention(db, task), - event, - outcomes, - catalog, - ), + input: eventInput(task, event, outcomes, catalog), metadata: { eventAutomationId: task.id }, replyAttribution: automationReplyAttribution(task.title), outcomes, diff --git a/packages/junior/src/chat/scheduled-automations/heartbeat.ts b/packages/junior/src/chat/scheduled-automations/heartbeat.ts index 4263e92b4a..55bb73b551 100644 --- a/packages/junior/src/chat/scheduled-automations/heartbeat.ts +++ b/packages/junior/src/chat/scheduled-automations/heartbeat.ts @@ -1,3 +1,4 @@ +import { slackMention } from "@/chat/slack/mrkdwn"; import { type Dispatch } from "@sentry/junior-plugin-api"; import { dispatchScheduledAutomation, @@ -9,7 +10,6 @@ import { getDb } from "@/chat/db"; import { logInfo } from "@/chat/logging"; import type { ConversationWorkQueue } from "@/chat/task-execution/queue"; import { notifyAutomationBlocked } from "@/chat/automations/blocked-notice"; -import { automationCreatorMention } from "@/chat/automations/creator"; import { settleEventAutomationRuns } from "@/chat/event-automations/runs"; import { automationReplyAttribution } from "@/chat/automations/reply-attribution"; import type { JuniorDatabase } from "@/db/db"; @@ -40,12 +40,9 @@ function singleLineMetadataValue(value: string): string { } /** Render the due scheduled automation as plain agent input. */ -async function buildDispatchInput( - db: JuniorDatabase, - task: ScheduledAutomation, -): Promise { +function buildDispatchInput(task: ScheduledAutomation): string { return renderTaskInput({ - creator: await automationCreatorMention(db, task), + creator: slackMention(task.createdBy.slackUserId), instructions: task.task.text, outcomes: effectiveTaskOutcomes(task.outcomes, task.destination), }); @@ -384,7 +381,7 @@ export async function runScheduledAutomationHeartbeat(args: { : undefined), destination: task.destination, destinationVisibility: task.conversationAccess.visibility, - input: await buildDispatchInput(db, task), + input: buildDispatchInput(task), metadata, replyAttribution: automationReplyAttribution(task.title), outcomes: effectiveTaskOutcomes(task.outcomes, task.destination), diff --git a/packages/junior/src/chat/scheduled-automations/tools/create.ts b/packages/junior/src/chat/scheduled-automations/tools/create.ts index 41977b07a8..7fe8947656 100644 --- a/packages/junior/src/chat/scheduled-automations/tools/create.ts +++ b/packages/junior/src/chat/scheduled-automations/tools/create.ts @@ -131,9 +131,7 @@ export function createSlackScheduleCreateAutomationTool( ); } - requireRequesterMention(input.instruction, context.actor, { - isCreator: true, - }); + requireRequesterMention(input.instruction, context.actor); const creator = await context.users.resolveActor(); const identity = creator?.identity; if ( diff --git a/packages/junior/src/chat/scheduled-automations/tools/update.ts b/packages/junior/src/chat/scheduled-automations/tools/update.ts index 1d32aa1a18..8367f4ec00 100644 --- a/packages/junior/src/chat/scheduled-automations/tools/update.ts +++ b/packages/junior/src/chat/scheduled-automations/tools/update.ts @@ -164,9 +164,7 @@ export function createSlackScheduleUpdateAutomationTool( input.instruction !== undefined && input.instruction !== lookup.task.text; if (input.instruction !== undefined && instructionChanged) { - requireRequesterMention(input.instruction, context.actor, { - isCreator, - }); + requireRequesterMention(input.instruction, context.actor); } const next = await editScheduledAutomation( lookup, diff --git a/packages/junior/src/chat/task-input.ts b/packages/junior/src/chat/task-input.ts index 316a983d07..c861a150ae 100644 --- a/packages/junior/src/chat/task-input.ts +++ b/packages/junior/src/chat/task-input.ts @@ -70,10 +70,7 @@ export function renderTaskInput(args: { outcomes?: TaskOutcome[]; /** Human label for the matched resource, when present. */ about?: string; - /** - * Creator mention for the Destination, rendered from the creator identity - * at run time. Instructions call this person "the Automation creator". - */ + /** Mention of the Automation creator, so "me" and "the Automation creator" are exact. */ creator?: string; /** Mentions of the people involved in an Event. Guidance says when to address them. */ people?: readonly string[]; @@ -106,13 +103,10 @@ export function renderTaskInput(args: { "This is a task, not a message from a person.", "", ...(about ? [`About: ${oneLine(about)}`] : []), - // Say who the creator is and what to write. With only who "me" is, the - // model often writes "your" and drops the mention. The full term keeps - // "issue creator" or "PR creator" from matching. Compatibility: older - // saved instructions can say "me" or "my", or hold the creator mention. + // Say what to write, or the model drops the mention. ...(creator ? [ - `Created by: ${oneLine(creator)}. This person is the Automation creator. Where the instructions say "the Automation creator", write this mention. "Me", "my", and this mention in the instructions also mean the Automation creator. If the instructions do not refer to the Automation creator, do not mention this person.`, + `Created by: ${oneLine(creator)}. This person is the Automation creator. "The Automation creator", "me", and "my" in the instructions mean this person; write this mention for them. If the instructions do not refer to this person, do not mention them.`, ] : []), ...(people.length > 0 ? [`People involved: ${people.join(", ")}`] : []), diff --git a/packages/junior/src/chat/tools/create-event-automation.ts b/packages/junior/src/chat/tools/create-event-automation.ts index 9fc73df20b..5eb1df8f5c 100644 --- a/packages/junior/src/chat/tools/create-event-automation.ts +++ b/packages/junior/src/chat/tools/create-event-automation.ts @@ -128,7 +128,7 @@ export function createEventAutomationTool( async execute(input, options) { const { actor, destination, source } = requireEventAutomationSlackContext(context); - requireRequesterMention(input.instruction, actor, { isCreator: true }); + requireRequesterMention(input.instruction, actor); const trigger = requireEventAutomationTrigger(catalog, input.trigger); const id = buildEventAutomationId({ channelId: destination.channelId, diff --git a/packages/junior/src/chat/tools/update-event-automation.ts b/packages/junior/src/chat/tools/update-event-automation.ts index 4bc9b69931..58c540e2f5 100644 --- a/packages/junior/src/chat/tools/update-event-automation.ts +++ b/packages/junior/src/chat/tools/update-event-automation.ts @@ -130,7 +130,7 @@ export function createUpdateEventAutomationTool( input.instruction != null && input.instruction !== current.task.text ) { - requireRequesterMention(input.instruction, actor, { isCreator }); + requireRequesterMention(input.instruction, actor); } let saved = current; if (edits) { diff --git a/packages/junior/tests/integration/event-automations.test.ts b/packages/junior/tests/integration/event-automations.test.ts index cfe49ee793..b206819d51 100644 --- a/packages/junior/tests/integration/event-automations.test.ts +++ b/packages/junior/tests/integration/event-automations.test.ts @@ -406,7 +406,7 @@ describe("event automations", () => { This is a task, not a message from a person. About: GitHub PR getsentry/junior#1174 - Created by: <@U123>. This person is the Automation creator. Where the instructions say "the Automation creator", write this mention. "Me", "my", and this mention in the instructions also mean the Automation creator. If the instructions do not refer to the Automation creator, do not mention this person. + Created by: <@U123>. This person is the Automation creator. "The Automation creator", "me", and "my" in the instructions mean this person; write this mention for them. If the instructions do not refer to this person, do not mention them. Instructions: Address the requested changes. Trusted summary: A reviewer requested changes. diff --git a/packages/junior/tests/integration/heartbeat-scheduled-automations.test.ts b/packages/junior/tests/integration/heartbeat-scheduled-automations.test.ts index b55625a5bc..b0cd86fd2a 100644 --- a/packages/junior/tests/integration/heartbeat-scheduled-automations.test.ts +++ b/packages/junior/tests/integration/heartbeat-scheduled-automations.test.ts @@ -200,9 +200,7 @@ describe("scheduled automation heartbeat", () => { const dispatchRecord = await getDispatchRecord(running!.dispatchId!); expect(dispatchRecord?.input).toContain("[task]"); expect(dispatchRecord?.input).not.toContain("[[NO_REPLY]]"); - // The creator identity decides the mention, not the createdBy copy. - expect(dispatchRecord?.input).toContain("Created by: <@U123>"); - expect(dispatchRecord?.input).not.toContain("U039RR91S"); + expect(dispatchRecord?.input).toContain("Created by: <@U039RR91S>"); expect(dispatchRecord?.input).toContain( "Instructions: Post a digest. Summarize the latest state.", ); diff --git a/packages/junior/tests/unit/chat/requester-mention.test.ts b/packages/junior/tests/unit/chat/requester-mention.test.ts index a72451d744..8a6b64db4f 100644 --- a/packages/junior/tests/unit/chat/requester-mention.test.ts +++ b/packages/junior/tests/unit/chat/requester-mention.test.ts @@ -12,11 +12,7 @@ const actor = { describe("requireRequesterMention", () => { it("rejects the requester's name when the mention is missing", () => { expect(() => - requireRequesterMention( - "Remind sam lee to submit the timesheet.", - actor, - { isCreator: true }, - ), + requireRequesterMention("Remind sam lee to submit the timesheet.", actor), ).toThrow( new ToolInputError( "The instruction names Sam Lee without a Slack mention. Write <@U0TEST> for this person.", @@ -31,30 +27,8 @@ describe("requireRequesterMention", () => { "Post the weekly digest.", // The name is part of another word. "Ask Sam Leeds for the report.", - // Older instructions and the creator's own edits can still say "me". - "Remind me to submit my timesheet.", ]) { - expect(() => - requireRequesterMention(instruction, actor, { isCreator: true }), - ).not.toThrow(); + expect(() => requireRequesterMention(instruction, actor)).not.toThrow(); } }); - - it('rejects "me" from a requester who did not create the Automation', () => { - // Each run reads "me" as the Automation creator, a different person. - expect(() => - requireRequesterMention("Remind me to review the PR.", actor, { - isCreator: false, - }), - ).toThrow( - new ToolInputError( - 'The requester did not create this Automation, and each run reads "me" and "my" as the Automation creator. Write <@U0TEST> for the requester.', - ), - ); - expect(() => - requireRequesterMention("Remind <@U0TEST> to review the PR.", actor, { - isCreator: false, - }), - ).not.toThrow(); - }); }); diff --git a/packages/junior/tests/unit/chat/task-input.test.ts b/packages/junior/tests/unit/chat/task-input.test.ts index 85310237cf..6fda3ef885 100644 --- a/packages/junior/tests/unit/chat/task-input.test.ts +++ b/packages/junior/tests/unit/chat/task-input.test.ts @@ -36,7 +36,7 @@ describe("renderTaskInput", () => { This is a task, not a message from a person. About: GitHub PR getsentry/junior#691 - Created by: <@U123>. This person is the Automation creator. Where the instructions say "the Automation creator", write this mention. "Me", "my", and this mention in the instructions also mean the Automation creator. If the instructions do not refer to the Automation creator, do not mention this person. + Created by: <@U123>. This person is the Automation creator. "The Automation creator", "me", and "my" in the instructions mean this person; write this mention for them. If the instructions do not refer to this person, do not mention them. People involved: <@U456> Done by: <@U456>. This person did what this Event describes. The Event calls them "octocat". That is the same person. They are also in People involved. Write to them as "you", and do not also call them "octocat". Instructions: Fix failed checks on this PR. From c570f8960a89b206b85da194d1e78ae7be974b3a Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:44:22 +0000 Subject: [PATCH 8/8] fix(automations): Say where to write the creator mention The integration evals on a8eeb4479 showed that "write this mention for them" lets the model drop the mention in both delivery cases. Say where to write it, as the earlier passing lines did. Say again in the tool description that each run knows the Automation creator, so the agent keeps the term instead of dropping the person. --- packages/junior-dashboard/src/mock-reporting/fixtures.ts | 2 +- packages/junior/src/chat/automations/edit-schema.ts | 2 +- packages/junior/src/chat/task-input.ts | 2 +- packages/junior/tests/integration/event-automations.test.ts | 2 +- packages/junior/tests/unit/chat/task-input.test.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/junior-dashboard/src/mock-reporting/fixtures.ts b/packages/junior-dashboard/src/mock-reporting/fixtures.ts index e6c2a4f19c..9d0df0bede 100644 --- a/packages/junior-dashboard/src/mock-reporting/fixtures.ts +++ b/packages/junior-dashboard/src/mock-reporting/fixtures.ts @@ -1451,7 +1451,7 @@ function eventAutomationRunConversation( "This is a task, not a message from a person.", "", "About: Issue · ACME-42", - 'Created by: <@U123>. This person is the Automation creator. "The Automation creator", "me", and "my" in the instructions mean this person; write this mention for them. If the instructions do not refer to this person, do not mention them.', + 'Created by: <@U123>. This person is the Automation creator. "The Automation creator", "me", and "my" in the instructions mean this person. Where the instructions refer to this person, write this mention in the message. If they do not, do not mention this person.', "Instructions: Summarize the closed issue", "", "Trusted summary: ACME-42 Checkout fails for saved cards was closed by Morgan Lee.", diff --git a/packages/junior/src/chat/automations/edit-schema.ts b/packages/junior/src/chat/automations/edit-schema.ts index 74f4de0ce7..01cbfa53c3 100644 --- a/packages/junior/src/chat/automations/edit-schema.ts +++ b/packages/junior/src/chat/automations/edit-schema.ts @@ -17,7 +17,7 @@ export const automationInstructionSchema = z.string().trim().min(1).max(4000); */ export const automationInstructionToolSchema = automationInstructionSchema.describe( - 'Work to perform when the Automation runs. The run does not see this conversation, so words like "this channel" or "here" will mean nothing then. Write the exact target instead, such as a channel mention or a link. Call the person who created the Automation "the Automation creator", not "me". Name every other person with their Slack mention, such as <@U123>, including a requester who did not create the Automation. Preserve the user\'s scope, targets, and constraints. Do not add permissions or side effects the user did not request.', + 'Work to perform when the Automation runs. The run does not see this conversation, so words like "this channel" or "here" will mean nothing then. Write the exact target instead, such as a channel mention or a link. Call the person who created the Automation "the Automation creator", not "me"; each run knows who that is. Name every other person with their Slack mention, such as <@U123>, including a requester who did not create the Automation. Preserve the user\'s scope, targets, and constraints. Do not add permissions or side effects the user did not request.', ); export const automationEditFieldsSchema = z.object({ diff --git a/packages/junior/src/chat/task-input.ts b/packages/junior/src/chat/task-input.ts index c861a150ae..3c38c97c9e 100644 --- a/packages/junior/src/chat/task-input.ts +++ b/packages/junior/src/chat/task-input.ts @@ -106,7 +106,7 @@ export function renderTaskInput(args: { // Say what to write, or the model drops the mention. ...(creator ? [ - `Created by: ${oneLine(creator)}. This person is the Automation creator. "The Automation creator", "me", and "my" in the instructions mean this person; write this mention for them. If the instructions do not refer to this person, do not mention them.`, + `Created by: ${oneLine(creator)}. This person is the Automation creator. "The Automation creator", "me", and "my" in the instructions mean this person. Where the instructions refer to this person, write this mention in the message. If they do not, do not mention this person.`, ] : []), ...(people.length > 0 ? [`People involved: ${people.join(", ")}`] : []), diff --git a/packages/junior/tests/integration/event-automations.test.ts b/packages/junior/tests/integration/event-automations.test.ts index b206819d51..0cae0b8200 100644 --- a/packages/junior/tests/integration/event-automations.test.ts +++ b/packages/junior/tests/integration/event-automations.test.ts @@ -406,7 +406,7 @@ describe("event automations", () => { This is a task, not a message from a person. About: GitHub PR getsentry/junior#1174 - Created by: <@U123>. This person is the Automation creator. "The Automation creator", "me", and "my" in the instructions mean this person; write this mention for them. If the instructions do not refer to this person, do not mention them. + Created by: <@U123>. This person is the Automation creator. "The Automation creator", "me", and "my" in the instructions mean this person. Where the instructions refer to this person, write this mention in the message. If they do not, do not mention this person. Instructions: Address the requested changes. Trusted summary: A reviewer requested changes. diff --git a/packages/junior/tests/unit/chat/task-input.test.ts b/packages/junior/tests/unit/chat/task-input.test.ts index 6fda3ef885..fd26bcf6ea 100644 --- a/packages/junior/tests/unit/chat/task-input.test.ts +++ b/packages/junior/tests/unit/chat/task-input.test.ts @@ -36,7 +36,7 @@ describe("renderTaskInput", () => { This is a task, not a message from a person. About: GitHub PR getsentry/junior#691 - Created by: <@U123>. This person is the Automation creator. "The Automation creator", "me", and "my" in the instructions mean this person; write this mention for them. If the instructions do not refer to this person, do not mention them. + Created by: <@U123>. This person is the Automation creator. "The Automation creator", "me", and "my" in the instructions mean this person. Where the instructions refer to this person, write this mention in the message. If they do not, do not mention this person. People involved: <@U456> Done by: <@U456>. This person did what this Event describes. The Event calls them "octocat". That is the same person. They are also in People involved. Write to them as "you", and do not also call them "octocat". Instructions: Fix failed checks on this PR.