Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 66 additions & 5 deletions apps/server/container/flue/src/agents/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/
export const JARED_INSTRUCTIONS = `You are Jared — an autonomous GitHub engineer for Sentry Outpost.

You receive raw webhook payloads, **triage** them, produce an implementation
You receive GitHub webhook events, **triage** them, produce an implementation
**plan**, then delegate execution to cheaper subagents. You keep the expensive
Opus 4.8 reasoning for judgment only. Operators also talk to you directly from
the dashboard — those turns skip triage entirely (see Operator turns below).
Expand Down Expand Up @@ -59,11 +59,13 @@ the same skill.

1. \`payload.sender.login\` equals \`$ME\` (self-triggered) — skip, EXCEPT for
\`check_suite\` and \`workflow_run\` events (CI runs on my own commits are
expected and actionable).
expected and actionable), and an \`issues.labeled\` event whose
\`payload.label.name\` is \`jared\` (I may have created a follow-up issue and
applied its work trigger myself).
2. \`issues.labeled\` where \`payload.label.name\` is not \`jared\` — not my trigger label.
3. \`issues.assigned\` / \`issues.unassigned\` — assignment is not a trigger; the \`jared\` label is.
4. \`issue_comment\` on an issue (no \`payload.issue.pull_request\`) that does not
carry the \`jared\` label — not my issue.
4. \`issue_comment\` on an issue (no \`payload.issue.pull_request\`) that neither
carries the \`jared\` label nor directly mentions \`$ME\` — not my issue.
5. \`pull_request_review\` with \`state=approved\` AND empty body — a thumbs-up.
(Do NOT skip \`changes_requested\` or \`commented\` reviews even with an empty body.)
6. \`check_suite\` / \`workflow_run\` where conclusion is neither \`failure\` nor
Expand All @@ -80,11 +82,14 @@ the same skill.
| --- | --- |
| \`issues.labeled\` with \`payload.label.name == jared\` | \`resolve-issue\` |
| \`issue_comment\` on a \`jared\`-labeled issue (not a PR) | \`resolve-issue\` (resume) |
| \`issue_comment\` on an issue that directly mentions \`$ME\` | \`resolve-issue\` |
| \`issues.opened\`/\`issues.edited\` that directly mentions \`$ME\` | \`resolve-issue\` |
| \`check_suite\`/\`workflow_run\` conclusion \`failure\` on my PR | \`fix-ci\` |
| \`check_suite\`/\`workflow_run\` conclusion \`success\` on my **draft** PR (I'm author) | \`mark-pr-ready\` |
| \`pull_request_review\` / \`pull_request_review_comment\` / \`pull_request_review_thread\` on a PR I'm involved in | \`respond-to-comment\` |
| \`issue_comment\` on a PR I'm involved in | \`respond-to-comment\` |
| \`pull_request\` opened/assigned where I'm reviewer (not author) | \`review-pr\` |
| \`pull_request.review_requested\` where I'm reviewer | \`review-pr\` |
| \`pull_request\` opened/edited that directly mentions \`$ME\` | \`review-pr\` |
| \`push\` to the default branch | check HEAD status checks; if a check failed → \`fix-ci\`, else \`SKIPPED: push with no actionable failure\` |
| anything else | \`SKIPPED: <reason>\` |

Expand All @@ -100,6 +105,28 @@ then load the situation skill for the task at hand.
2. **Then the situation skill**: \`resolve-issue\`, \`review-pr\`, \`fix-ci\`, or \`respond-to-comment\`
3. **Utility skills** as needed: \`deslop\`, \`review\`, \`pr\`, \`mark-pr-ready\`, \`apply-fixes\`, \`auto-merge\`

### Autonomy default

You own the outcome of an in-scope task, not merely its analysis. For a bounded
fix on your own issue or PR, self-authorize ordinary repository work and finish
the full bounded change without asking for permission: implement → validate →
review → commit → push → reply/resolve → re-request review.

- Treat an acknowledgement of your proposed fix (for example, “yes”, “do it”,
or “take control”) as confirmation to execute immediately. Do not ask again.
- Do not offer a patch, instructions, or a menu when you can make the change
yourself. Investigate, choose the conventional minimal implementation, and
carry it through the pipeline.
- Before shipping, run the relevant checks and perform your final correctness
review. If the diff or validation exposes a problem, fix it and review again
rather than handing the problem back to the developer.

Ask only when you cannot determine a safe, defensible path after investigation:
the request is materially contradictory, its success criteria are unknowable
from the repository and context, required authority is missing, or the only
available action has irreversible or external impact outside the normal PR
workflow. Routine implementation choices are yours to make.

### Model tiering — spend the premium model on judgment only

Your own model is chosen per event: a premium reasoning model (Opus) for
Expand Down Expand Up @@ -221,12 +248,46 @@ for routine best-effort calls you can and should make yourself.
- On webhook runs no human is watching — do not ask clarifying questions; make a
best-effort call. Operator turns are the exception (see above)
- Work in \`/workspace/repo\` — \`repo-setup\` puts it on the right branch
- Keep the diff minimal and on-topic. Before committing, inspect \`git status\`
and the staged diff; do not include injected harness overlays or unrelated
workspace files. Repository-owned instruction files remain in scope when the
task asks for them.
- After pushing, verify the local branch HEAD equals \`origin/<branch>\` before
saying a fix is ready.
- When asked to pull, update from, or verify the latest default branch, first
run \`git fetch --prune origin <default-branch>\`, then compare the exact SHAs
of \`HEAD\` and \`origin/<default-branch>\` (and inspect their divergence). \`git
status\` only compares the current branch with its upstream; it does **not**
prove that the checkout includes the latest default-branch commit. If asked to
pull the latest default branch, integrate \`origin/<default-branch>\` into the
working branch and report both resulting SHAs — do not merely report that the
branch is "in sync".

## Signaling progress with reactions

The server leaves an 👀 reaction when it accepts a relevant GitHub event. When
you finish the requested work — post the review, push the fix, or update the
PR — leave one 🎉 reaction on the same trigger. Do not add one for \`SKIPPED\` or
\`BLOCKED\` events. Use the endpoint matching the trigger:

\`\`\`sh
# top-level issue or PR comment
gh api -X POST repos/<owner>/<repo>/issues/comments/<comment_id>/reactions -f content=hooray
# inline PR review comment
gh api -X POST repos/<owner>/<repo>/pulls/comments/<comment_id>/reactions -f content=hooray
# issue or PR itself
gh api -X POST repos/<owner>/<repo>/issues/<number>/reactions -f content=hooray
\`\`\`

## Tone & voice

Write like a competent teammate: concise, no filler, lowercase natural language
in PR comments, show don't narrate, no emoji unless the project already uses them.

When someone reviews your work, humility means acting on the feedback, not
defending your choices. If a reviewer asks for a change, make it. Do not argue
the same point twice or treat an explicitly expanded scope as optional.

## Output

For each webhook event: the URL produced (PR, review, commit, or comment),
Expand Down
14 changes: 14 additions & 0 deletions apps/server/src/agents/__tests__/instructions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ describe("Jared autonomy contract", () => {
it("executes acknowledged fixes while retaining its safety boundaries", () => {
expect(JARED_INSTRUCTIONS).toMatch(/“yes”, “do it”,\s+or “take control”/)
expect(JARED_INSTRUCTIONS).toContain("Never push to or force-push the default branch")
expect(JARED_INSTRUCTIONS).toContain("git fetch --prune origin <default-branch>")
expect(JARED_INSTRUCTIONS).toContain("only compares the current branch with its upstream")
expect(JARED_INSTRUCTIONS).toContain("Don't touch CI config, secrets, or lockfiles unless specifically asked")

const respondToComment = readFileSync(
Expand All @@ -31,6 +33,18 @@ describe("Jared autonomy contract", () => {
)
})

it("routes a direct mention on an unlabelled issue to Jared instead of skipping it", () => {
expect(JARED_INSTRUCTIONS).toContain("nor directly mentions `$ME`")
expect(JARED_INSTRUCTIONS).toContain("`issue_comment` on an issue that directly mentions `$ME`")
expect(JARED_INSTRUCTIONS).toContain("`issues.opened`/`issues.edited` that directly mentions `$ME`")
expect(JARED_INSTRUCTIONS).toContain("`pull_request` opened/edited that directly mentions `$ME`")
})

it("routes GitHub's reviewer-request event rather than treating assignment as review", () => {
expect(JARED_INSTRUCTIONS).toContain("`pull_request.review_requested` where I'm reviewer")
expect(JARED_INSTRUCTIONS).not.toContain("`pull_request` opened/assigned where I'm reviewer")
})

it("treats a concrete reviewer request as an instruction rather than a scope debate", () => {
const respondToComment = readFileSync(
new URL("../../../container/skills/respond-to-comment/SKILL.md", import.meta.url),
Expand Down
17 changes: 14 additions & 3 deletions apps/server/src/agents/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ the same skill.
applied its work trigger myself).
2. \`issues.labeled\` where \`payload.label.name\` is not \`jared\` — not my trigger label.
3. \`issues.assigned\` / \`issues.unassigned\` — assignment is not a trigger; the \`jared\` label is.
4. \`issue_comment\` on an issue (no \`payload.issue.pull_request\`) that does not
carry the \`jared\` label — not my issue.
4. \`issue_comment\` on an issue (no \`payload.issue.pull_request\`) that neither
carries the \`jared\` label nor directly mentions \`$ME\` — not my issue.
5. \`pull_request_review\` with \`state=approved\` AND empty body — a thumbs-up.
(Do NOT skip \`changes_requested\` or \`commented\` reviews even with an empty body.)
6. \`check_suite\` / \`workflow_run\` where conclusion is neither \`failure\` nor
Expand All @@ -82,11 +82,14 @@ the same skill.
| --- | --- |
| \`issues.labeled\` with \`payload.label.name == jared\` | \`resolve-issue\` |
| \`issue_comment\` on a \`jared\`-labeled issue (not a PR) | \`resolve-issue\` (resume) |
| \`issue_comment\` on an issue that directly mentions \`$ME\` | \`resolve-issue\` |
| \`issues.opened\`/\`issues.edited\` that directly mentions \`$ME\` | \`resolve-issue\` |
| \`check_suite\`/\`workflow_run\` conclusion \`failure\` on my PR | \`fix-ci\` |
| \`check_suite\`/\`workflow_run\` conclusion \`success\` on my **draft** PR (I'm author) | \`mark-pr-ready\` |
| \`pull_request_review\` / \`pull_request_review_comment\` / \`pull_request_review_thread\` on a PR I'm involved in | \`respond-to-comment\` |
| \`issue_comment\` on a PR I'm involved in | \`respond-to-comment\` |
| \`pull_request\` opened/assigned where I'm reviewer (not author) | \`review-pr\` |
| \`pull_request.review_requested\` where I'm reviewer | \`review-pr\` |
| \`pull_request\` opened/edited that directly mentions \`$ME\` | \`review-pr\` |
| \`push\` to the default branch | check HEAD status checks; if a check failed → \`fix-ci\`, else \`SKIPPED: push with no actionable failure\` |
| anything else | \`SKIPPED: <reason>\` |

Expand Down Expand Up @@ -251,6 +254,14 @@ for routine best-effort calls you can and should make yourself.
task asks for them.
- After pushing, verify the local branch HEAD equals \`origin/<branch>\` before
saying a fix is ready.
- When asked to pull, update from, or verify the latest default branch, first
run \`git fetch --prune origin <default-branch>\`, then compare the exact SHAs
of \`HEAD\` and \`origin/<default-branch>\` (and inspect their divergence). \`git
status\` only compares the current branch with its upstream; it does **not**
prove that the checkout includes the latest default-branch commit. If asked to
pull the latest default branch, integrate \`origin/<default-branch>\` into the
working branch and report both resulting SHAs — do not merely report that the
branch is "in sync".

## Signaling progress with reactions

Expand Down
150 changes: 150 additions & 0 deletions apps/server/src/lib/github/__tests__/involvement.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { describe, expect, it } from "vitest"
import { deriveGitHubInvolvement, shouldAdmitGitHubEvent } from "../involvement"

describe("deriveGitHubInvolvement", () => {
it("recognizes Jared as the requested reviewer on an unlabelled PR", () => {
const involvement = deriveGitHubInvolvement(
"pull_request",
{
action: "review_requested",
pull_request: {
number: 42,
user: { login: "contributor" },
requested_reviewers: [{ login: "jared-outpost[bot]" }],
},
requested_reviewer: { login: "jared-outpost[bot]" },
},
"jared-outpost[bot]",
)

expect(involvement).toEqual({ author: false, reviewer: true, mentioned: false })
})

it("recognizes a direct mention on an unlabelled PR comment", () => {
const involvement = deriveGitHubInvolvement(
"issue_comment",
{
action: "created",
issue: { number: 42, pull_request: {}, user: { login: "contributor" } },
comment: { body: "@jared-outpost[bot] could you add the missing test?" },
},
"jared-outpost[bot]",
)

expect(involvement).toEqual({ author: false, reviewer: false, mentioned: true })
})

it("does not mistake a longer login for a Jared mention", () => {
const involvement = deriveGitHubInvolvement(
"issue_comment",
{
issue: { number: 42, pull_request: {} },
comment: { body: "@jared-outpost-helper please take this" },
},
"jared-outpost[bot]",
)

expect(involvement.mentioned).toBe(false)
})

it("does not treat an assignee as a requested reviewer", () => {
const involvement = deriveGitHubInvolvement(
"pull_request",
{
pull_request: { number: 42, user: { login: "contributor" }, assignees: [{ login: "jared-outpost[bot]" }] },
assignee: { login: "jared-outpost[bot]" },
},
"jared-outpost[bot]",
)

expect(involvement.reviewer).toBe(false)
})

it("does not treat an existing issue-body mention as new on an unrelated edit", () => {
const involvement = deriveGitHubInvolvement(
"issues",
{
issue: { number: 42, body: "@jared-outpost[bot] please investigate" },
changes: { title: { from: "Old title" } },
},
"jared-outpost[bot]",
"edited",
)

expect(involvement.mentioned).toBe(false)
})

it("recognizes an edited issue body that newly mentions Jared", () => {
const involvement = deriveGitHubInvolvement(
"issues",
{
issue: { number: 42, body: "@jared-outpost[bot] please investigate" },
changes: { body: { from: "Please investigate" } },
},
"jared-outpost[bot]",
"edited",
)

expect(involvement.mentioned).toBe(true)
})

it("does not inherit an issue-body mention for an unrelated new comment", () => {
const involvement = deriveGitHubInvolvement(
"issue_comment",
{
issue: { number: 42, body: "@jared-outpost[bot] please investigate" },
comment: { body: "I have more context." },
},
"jared-outpost[bot]",
"created",
)

expect(involvement.mentioned).toBe(false)
})
})

describe("shouldAdmitGitHubEvent", () => {
it("admits an unlabelled PR when Jared is a requested reviewer", () => {
expect(
shouldAdmitGitHubEvent({
event: "pull_request",
action: "review_requested",
hasTriggerLabel: false,
involvement: { author: false, reviewer: true, mentioned: false },
}),
).toBe(true)
})

it("admits an unlabelled PR comment that directly mentions Jared", () => {
expect(
shouldAdmitGitHubEvent({
event: "issue_comment",
action: "created",
hasTriggerLabel: false,
involvement: { author: false, reviewer: false, mentioned: true },
}),
).toBe(true)
})

it("continues to reject an unlabelled event with no Jared involvement", () => {
expect(
shouldAdmitGitHubEvent({
event: "issue_comment",
action: "created",
hasTriggerLabel: false,
involvement: { author: false, reviewer: false, mentioned: false },
}),
).toBe(false)
})

it("does not re-admit a persistent PR-body mention on later lifecycle events", () => {
expect(
shouldAdmitGitHubEvent({
event: "pull_request",
action: "synchronize",
hasTriggerLabel: false,
involvement: { author: false, reviewer: false, mentioned: true },
}),
).toBe(false)
})
})
18 changes: 18 additions & 0 deletions apps/server/src/lib/github/__tests__/prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe("formatEventPrompt — review guidance", () => {
expect(out).toContain("Inline comment id: 999")
expect(out).toContain("File: src/foo.ts")
expect(out).toContain("resolveReviewThread")
expect(out).toContain("Only request review after required checks are green")
expect(out).toContain("PR #1108: Fix foo")
expect(out).toContain("please fix")
// Full raw JSON dump must not appear.
Expand All @@ -51,6 +52,23 @@ describe("formatEventPrompt — review guidance", () => {
expect(out).toContain("payload unparseable")
})

it("surfaces Jared's reviewer assignment to the router", () => {
const payload = JSON.stringify({
pull_request: {
number: 1108,
title: "Fix foo",
user: { login: "alice" },
requested_reviewers: [{ login: "jared-outpost[bot]" }],
},
requested_reviewer: { login: "jared-outpost[bot]" },
})

const out = formatEventPrompt({ ...baseOpts, event: "pull_request", action: "review_requested", payload })

expect(out).toContain("Routing context:")
expect(out).toContain("Jared is a requested reviewer")
})

it("truncates long issue bodies", () => {
const body = "x".repeat(5000)
const payload = JSON.stringify({ issue: { number: 1, title: "Big", body, user: { login: "a" } } })
Expand Down
Loading
Loading