Skip to content
Merged
4 changes: 3 additions & 1 deletion .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"**/emdash-env.d.ts",
"**/worker-configuration.d.ts",
"packages/registry-lexicons/src/generated/**",
"packages/plugin-cli/schemas/**"
"packages/plugin-cli/schemas/**",
"infra/emdash-bot/.flue/lib/machine.json",
"infra/emdash-bot/BOT_STATE_MACHINE.md"
]
}
54 changes: 38 additions & 16 deletions infra/emdash-bot/.flue/agents/investigate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const SANDBOX_EXEC_GRACE_MS = 30_000;
const initialDataSchema = v.object({
runId: v.pipe(v.string(), v.minLength(1)),
issueNumber: v.number(),
mode: v.picklist(["repro", "implement", "revise"]),
mode: v.picklist(["repro", "implement", "revise", "diagnose", "fix"]),
arg: v.optional(v.nullable(v.string())),
issueTitle: v.pipe(v.string(), v.minLength(1)),
issueBody: v.string(),
Expand Down Expand Up @@ -173,12 +173,17 @@ async function setupSandbox(
if (!repo) throw new Error("repository context is not configured");
const cloneUrl = `https://github.com/${repo.owner}/${repo.repo}.git`;
const branch = input.mode === "revise" ? `bot/fix-${input.issueNumber}` : "main";
const pushCapability = await createPushCapability(
workerEnv.GITHUB_WEBHOOK_SECRET,
repo.owner,
repo.repo,
input.issueNumber,
);
// Diagnose mode is investigation-only: no push capability enters the
// sandbox, so a fix push is impossible rather than merely instructed against.
const pushCapability =
input.mode === "diagnose"
? null
: await createPushCapability(
workerEnv.GITHUB_WEBHOOK_SECRET,
repo.owner,
repo.repo,
input.issueNumber,
);
const steps: Array<{ name: string; command: string; timeoutMs?: number; nonFatal?: boolean }> = [
{
name: "git-identity-email",
Expand All @@ -200,10 +205,14 @@ async function setupSandbox(
name: "checkout-main",
command: `cd ${REPO_DIR} && git checkout main && git reset --hard origin/main`,
},
{
name: "git-push-capability",
command: `cd ${REPO_DIR} && git config http.https://github.com/.extraHeader '${PUSH_CAPABILITY_HEADER}: ${pushCapability}'`,
},
...(pushCapability
? [
{
name: "git-push-capability",
command: `cd ${REPO_DIR} && git config http.https://github.com/.extraHeader '${PUSH_CAPABILITY_HEADER}: ${pushCapability}'`,
},
]
: []),
{
name: "pnpm-install",
command: `cd ${REPO_DIR} && pnpm install --frozen-lockfile --prefer-offline`,
Expand Down Expand Up @@ -268,6 +277,22 @@ async function detectPush(issueNumber: number, previousBranchSha: string | null)

function buildPrompt(input: InvestigateData): string {
const argSection = input.arg ? ["", "## Directive", "", input.arg, ""].join("\n") : "";
const diagnose = input.mode === "diagnose";
const method = diagnose
? [
"- Read AGENTS.md, find the relevant code, and attempt to reproduce the bug.",
"- Diagnose the root cause. Do NOT write or push a fix -- this is investigation only.",
"- Report `reproduced` and put the diagnosis in `summary`. Use verdict `unclear` only when you are blocked on information that only the reporter can supply.",
]
: [
"- Read AGENTS.md, find the relevant code, attempt to reproduce, build, or revise.",
"- Write tests where they make sense.",
"- Touch only files relevant to the issue. Do not bulk-format or modify .github/workflows.",
`- When done, commit and push: \`git checkout -B bot/fix-${input.issueNumber} && git add <files> && git commit -m '<message>' && git push -u origin HEAD --force-with-lease\`.`,
];
const closing = diagnose
? "Call report_result exactly once when finished. Do not set fixed; report reproduced and your verdict with the diagnosis in summary."
: "Call report_result exactly once when finished. fixed may only be true if a fix and test passed and the branch was pushed.";
return [
`Investigate issue #${input.issueNumber} in mode: ${input.mode}.`,
"",
Expand All @@ -279,11 +304,8 @@ function buildPrompt(input: InvestigateData): string {
argSection,
"## Method",
"",
"- Read AGENTS.md, find the relevant code, attempt to reproduce, build, or revise.",
"- Write tests where they make sense.",
"- Touch only files relevant to the issue. Do not bulk-format or modify .github/workflows.",
`- When done, commit and push: \`git checkout -B bot/fix-${input.issueNumber} && git add <files> && git commit -m '<message>' && git push -u origin HEAD --force-with-lease\`.`,
...method,
"",
"Call report_result exactly once when finished. fixed may only be true if a fix and test passed and the branch was pushed.",
closing,
].join("\n");
}
23 changes: 23 additions & 0 deletions infra/emdash-bot/.flue/lib/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ export function renderReadonlyReply(state: StateId | null): string {
return "I declined this. Reopen with `@emdashbot reopen` if circumstances change.";
case "failed":
return "My last attempt failed. A maintainer can `@emdashbot retry` or take it over.";
case "investigating":
return "Investigating now (reproduce + diagnose). I'll report a verdict with evidence.";
case "reproduced":
return "Reproduced it -- diagnosis in my last comment. A maintainer can `@emdashbot fix` to try a fix, or `@emdashbot decline`.";
case "not_reproduced":
return "I couldn't reproduce this; transcript above. Reply with steps that fail for you, or a maintainer can `@emdashbot decline`.";
case "needs_info":
return "I need more to go on -- see my last comment for what's missing.";
case "fixing":
return "Building a candidate fix.";
case "preview_building":
return "Building a preview so you can try the fix.";
case "awaiting_reporter":
return "Try the preview from my last comment. Reply `@emdashbot confirm` if it's fixed, or describe what's still wrong.";
default: {
const _exhaustive: never = state;
return `State: \`${String(_exhaustive)}\`.`;
Expand All @@ -52,6 +66,11 @@ export function renderAgentComment(

switch (decision.event) {
case "agent.fix_ready":
// The fix loop routes fix_ready into preview_building, where the preview
// pipeline posts a deployed-preview link on preview.ready; a pkg.pr.new
// install line is the legacy awaiting_feedback lane only.
if (decision.to === "preview_building")
return `${summary}\n\nBuilding a preview so you can try the change before I open a PR.`;
return [
summary,
"",
Expand All @@ -64,9 +83,13 @@ export function renderAgentComment(
"Reply `@emdashbot confirm` if it works and I'll open the PR, or `@emdashbot revise <feedback>` to push changes.",
].join("\n");
case "agent.reproduced":
if (decision.to === "reproduced")
return `${summary}\n\nA maintainer can \`@emdashbot fix\` to try a fix, or \`@emdashbot decline\`.`;
return `${summary}\n\nReply \`@emdashbot implement <directive>\` if you want me to take another swing with guidance.`;
case "agent.not_reproduced":
return `${summary}\n\nReply with steps that fail for you, or close if it's no longer relevant.`;
case "agent.needs_info":
return `${summary}\n\nReply with the details above; a maintainer can \`@emdashbot investigate\` again once they arrive.`;
default:
return summary;
}
Expand Down
13 changes: 12 additions & 1 deletion infra/emdash-bot/.flue/lib/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,16 @@ export async function getBranchSha(
return json.commit?.sha ?? null;
}

/** Deletes a branch ref. A 404/422 means it is already gone, which is fine. */
export async function deleteBranch(token: string, ctx: RepoContext, branch: string): Promise<void> {
const res = await githubFetch(
`${GITHUB_API}/repos/${ctx.owner}/${ctx.repo}/git/refs/heads/${encodeURIComponent(branch)}`,
{ method: "DELETE", headers: authHeaders(token) },
);
if (res.status === 404 || res.status === 422) return;
if (!res.ok) throw new Error(`deleteBranch(${branch}) failed: ${res.status} ${await res.text()}`);
}

export async function addLabels(
token: string,
ctx: RepoContext,
Expand Down Expand Up @@ -252,7 +262,7 @@ export async function getOpenPullRequest(
export async function createPullRequest(
token: string,
ctx: RepoContext,
args: { headBranch: string; baseBranch: string; title: string; body: string },
args: { headBranch: string; baseBranch: string; title: string; body: string; draft?: boolean },
): Promise<CreatedPullRequest> {
const res = await githubFetch(`${GITHUB_API}/repos/${ctx.owner}/${ctx.repo}/pulls`, {
method: "POST",
Expand All @@ -262,6 +272,7 @@ export async function createPullRequest(
base: args.baseBranch,
title: args.title,
body: args.body,
draft: args.draft === true,
}),
});
if (!res.ok) {
Expand Down
Loading
Loading