Skip to content

chore(emdash-bot): migrate to Flue 2 agents#2101

Open
ascorbic wants to merge 33 commits into
mainfrom
feat/emdash-bot-worker
Open

chore(emdash-bot): migrate to Flue 2 agents#2101
ascorbic wants to merge 33 commits into
mainfrom
feat/emdash-bot-worker

Conversation

@ascorbic

@ascorbic ascorbic commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Migrates emdashbot from Flue 1 workflows to Flue 2 agents and makes the Durable Object the canonical source of orchestration state.

  • Adds durable webhook admission, ordered GitHub projections, dispatch recovery, and stale-run recovery.
  • Moves classification and investigation onto Flue 2 agents with bounded Sandbox RPCs and a repository-scoped GitHub proxy.
  • Adds the v3 Durable Object migration after directly verifying that production has no active Flue 1 runs.

Closes: N/A, internal emdashbot infrastructure migration.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (not applicable: no admin UI changes)
  • I have added a changeset (not applicable: only private infrastructure changed)
  • New features link to an approved Discussion (not applicable: internal infrastructure migration)

AI-generated code disclosure

  • This PR includes AI-generated code, model/tool: GPT-5.6 Sol

Screenshots / test output

No visual changes. Verified with the full workspace build and package typecheck, full type-aware lint, formatting checks, 70 bot unit tests, and 26 Workers integration tests.


Try this PR

Open a fresh playground →

A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.

Tracks feat/emdash-bot-worker. Updated automatically when the playground redeploys.

ascorbic added 30 commits July 17, 2026 20:59
The state-machine work from the closed feat/bot-state-machine branch is
being rebuilt on Cloudflare per .opencode/plans/0003-bot-on-cloudflare.md.

Phase 0 is the integration spike: stand up a Cloudflare-target Flue app
with the container-backed Sandbox integration and Workers AI binding,
prove the build succeeds, then validate end-to-end locally.

This commit lands the scaffolding:

- package.json / tsconfig.json mirroring infra/flue-review.
- wrangler.jsonc: AI binding, Sandbox + FlueRegistry DOs, container
  declaration, R2 workspace bucket. No Orchestrator DO yet (Phase 3).
- Dockerfile: minimal base for the spike (Node + pnpm + git + gh). The
  full toolchain (Chromium, agent-browser) lands in Phase 2 when we
  port the repro skills.
- .flue/cloudflare.ts: re-exports Sandbox DO, narrows Env.Sandbox type.
- .flue/app.ts: Hono root with Flue's standard /workflows routing.
- .flue/lib/classifier.ts: the shared classifier agent, defaulting to
  qwen3-30b-a3b-fp8 via the cloudflare/* binding-native provider id
  (no API key in scope).
- .flue/workflows/classify-command.ts: state-aware free-text classifier,
  ported from the previous attempt with cf-wai/* model strings replaced
  by cloudflare/@cf/* binding strings.
- .flue/workflows/investigate.ts: minimal stub agent that runs inside
  the container-backed Sandbox via cloudflareSandbox(getSandbox(...)).
  Returns structured classification only -- real five-stage pipeline
  lands in Phase 2.

Verified: `pnpm typecheck` and `pnpm build` clean. Both workflows
discovered. Sandbox container declaration parses. The Flue beta.5 +
@cloudflare/sandbox 0.12.1 + agents 0.14.5 + hono 4.12.23 dependency
set installs and resolves cleanly under the workspace's release-age
policy.
Both halves work locally:

  classify-command (qwen3-30b via env.AI binding):
    POST /workflows/classify-command?wait=result
    -> {event:'retry', tokens:{input:1741,output:292}, model:'cloudflare/@cf/qwen/qwen3-30b-a3b-fp8'}

  investigate (glm-5.2 inside Cloudflare Sandbox container):
    POST /workflows/investigate?wait=result
    -> {kind:'bug', area:'admin', requiresBrowser:true, tokens:{input:1624,output:60},
        model:'cloudflare/@cf/zai-org/glm-5.2'}

The structural credential boundary works as designed: no API key is in the
agent's process address space at any point. The whole class of bugs the
previous attempt's eight review cycles patched is gone.

Fixes since first scaffold:

- Dropped process.env references from the agent definitions (Workers have
  no process.env; Flue rejected the AgentDefinition shape at runtime).

- Dockerfile: switched to a multi-stage build that COPIES Node + npm + pnpm
  from the official node:22.21.0-bookworm-slim image rather than downloading
  inside the sandbox base. The Cloudflare Sandbox base image has a broken
  CA bundle that fails curl -fsSL against nodejs.org with 'self-signed
  certificate in certificate chain'; copying across avoids any TLS in the
  final image. Symlinks for npm/pnpm CLIs get rebuilt because Docker COPY
  flattens slim's symlinks; chmod +x on the .cjs targets because they lose
  their executable bit during COPY.

- wrangler.jsonc: added Flue's workflow DO classes (FlueClassifyCommand-
  Workflow, FlueInvestigateWorkflow) plus FlueRegistry to new_sqlite_classes.
  Flue auto-adds the durable_objects.bindings in its merged .flue-vite.
  wrangler.jsonc but does not add the migrations; the user has to declare
  them sqlite-enabled. Same pattern as flue-review's wrangler.jsonc.

Phase 0 decision gate: PASS. Browser support (Phase 2) is the remaining
load-bearing piece -- the spike only proves the agent runs in a container
with Workers AI, not that it can drive Chromium for repro-admin/public
skills. That risk gates Phase 2, not the architecture choice.
Phase 0 stretch validation. Extended the Dockerfile with Chromium's
runtime dependencies (libnss3, libatk, fonts-liberation, etc.) and
copied bgproc + agent-browser from the node-source stage. Added a
throwaway sandbox-probe workflow that runs a sequence of shell commands
inside the container to verify each piece.

Confirmed inside the Sandbox container:
- Node 22.21.0, pnpm 11.1.3, git 2.34.1, bgproc, agent-browser 0.30.1.
- 'agent-browser install' downloads Chrome 150 (~179 MB) in ~50s.
- Chromium launches and loads pages; agent-browser open/read works.

Two cleanup items for Phase 1 (not blockers):

- Chromium reinstalls every run because each Sandbox DO call gets a
  fresh $HOME. Phase 1 should either bake Chromium into the Dockerfile
  (~200 MB image growth) or persist /root/.agent-browser/browsers
  across runs via a volume / R2 cache.

- Locally, Cloudflare WARP intercepts TLS with a corp Zero Trust cert
  the sandbox base doesn't trust, so chromium shows cert warnings on
  page loads. This is dev-machine only and won't happen on real CF
  edges; safe to defer.

Decision: Phase 0 done. Browser-in-Sandbox works. Phase 1 (orchestrator
DO + webhook ingress + real investigate pipeline port) can proceed.
Ports machine.ts and router.cjs from the closed feat/bot-state-machine
branch into infra/emdash-bot/.flue/lib/. router.ts re-exports
findTransition from machine.ts (single source of truth) and uses
TypeScript-native exports instead of the CommonJS wrapper.

Sets up vitest in tests/unit/ with the full 32-case router suite ported
from node:test. All pass in 4ms. tsconfig includes tests/ for typecheck.

These are the substrate-agnostic bits of the bot logic; the OrchestratorDO
will wrap them with per-issue serialization in the next commit.
Adds the per-issue Orchestrator Durable Object as the source of truth for
the bot lifecycle. The DO holds state, kind, currentRunId, prNumber, and
a bounded event log; webhook events route through event() and the
intrinsic per-instance serialization eliminates the PR-comment race that
plagued the Actions-based predecessor (PR #1606 cycle 4).

This commit is the SKELETON: resolve + persist + delivery dedupe + stale-
run discard + inert-state guard. GitHub side effects (label flip, comment,
PR ops) and workflow invocation land in the next commit alongside the
webhook handler, since they share the App-token-issuing helper.

Test infrastructure also lands here. Pure router tests stay in tests/unit/
(plain vitest, ~4ms). DO tests live in tests/integration/ and run under
@cloudflare/vitest-pool-workers against a real workerd isolate, with a
separate wrangler.test.jsonc + test entry that mirrors prod bindings but
skips Flue-generated workflow DOs (not exported by the test entry). Both
suites run via pnpm test; 32 unit + 7 integration tests pass.
POST /webhook/github verifies the X-Hub-Signature-256 HMAC against the
raw body, normalizes the GitHub payload into a NormalizedEvent, and
dispatches into the per-anchor Orchestrator DO via getByName.

Handles issues, issue_comment, pull_request, pull_request_review, and
pull_request_review_comment events. The classifier hand-off lives in the
DO; the webhook resolves bare verbs deterministically (parseCommand) and
flags everything else as needsClassify. Bot-author detection for the
in_review default-comment-event is intentionally deferred (defaults to
false, routes through classifier) until we have a known bot login binding.

Tests:
- 25 unit tests for actor classification + payload normalization (pure)
- 11 integration tests under @cloudflare/vitest-pool-workers: signature
  verify (the workerd-only timingSafeEqual), full SELF.fetch -> DO round
  trips for the deterministic verb path, free-text classify-pending path,
  duplicate-delivery dedupe, and signature/JSON rejection.

The core routes (health + webhook) live in routes.ts so the workers-pool
test entry can mount them without pulling in Flue's workflow routes
(those need workflow DOs the test wrangler does not declare). Production
app.ts still mounts both.
Ports readAppCreds + mintInstallationToken + JWT helpers from
infra/flue-review, adds bot-specific API helpers (addLabels, removeLabel,
postIssueComment, getIssueLabels), and wires them into the orchestrator's
event() path.

The transition path now: resolve -> applySideEffects (label flip + comment)
-> persistDecision. Side effects run BEFORE persist so a label-flip
failure leaves DO state unchanged and the next event retries; comment
failures are non-fatal and logged.

Tokens are cached in DO storage with a 55-minute window. Dev mode (no
GITHUB_APP_PRIVATE_KEY) cleanly skips all GitHub calls with a log line --
no exceptions, DO state still advances locally.

Adds GITHUB_OWNER + GITHUB_REPO as wrangler vars for the single managed
repo. Multi-repo support deferred; would need owner/repo plumbed through
NormalizedEvent and the DO signature.

The webhook normalizer now stamps anchorNumber onto every NormalizedEvent
so the DO can address the GitHub side. Tests stay green via empty PEM
binding -> readAppCreds returns null -> side effects skip.
Free-text comments now invoke the classify-command workflow synchronously
via ctx.exports.default (the loopback service binding to the Worker's
default fetch handler, no service-binding config needed). The DO blocks
~1-2s on the classifier turn, then re-enters event() with the resolved
verb.

Removes the classify-pending placeholder outcome -- the DO now always
returns a concrete decision (transition / readonly / noop), where
classifier failures degrade to noop with a logged error.

Updates tests to the cloudflare:workers exports surface (cloudflare:test
is deprecated as of vitest-pool-workers v0.13).
Adds runAction() to the orchestrator: on a transition with decision.action
(investigate.repro / investigate.implement / investigate.revise), fetches
the issue context via the GitHub App token, generates a runId, persists
it as currentRunId, and admits the investigate workflow via Flue's
invoke().

Extends the investigate workflow to accept the orchestrator's input shape
(runId, mode, arg, issueTitle, issueBody) and call back into the right
OrchestratorDO via env.Orchestrator.getByName(...).applyAgentResult() on
completion. Phase 1 body is still a single LLM call; the 5-stage pipeline
is Phase 2.

Wires DO and Worker exports through .flue/wrangler-main.ts so wrangler
types can infer the class generics on Cloudflare.Env's DurableObjectName-
space bindings and the Cloudflare.Exports loopback service shape. The
file is type-only -- flue dev generates its own runtime entry and the
default fetch handler here just returns 500 if hit.

Drops the previous ad-hoc Env augmentation in cloudflare.ts. No tests
needed updating: the existing integration suite already exercises the
new action path (with creds-null degrading to a logged skip).
Status / help mentions used to be no-ops on GitHub. The orchestrator now
posts a short reply (current state + offered commands footer) when a
readonly event lands. No-creds dev mode still skips silently.
Adds self-arming alarms on the OrchestratorDO. The tick recovers stale
runs (currentRunId older than 30 minutes -> drop, allowing a retry) and
reconciles label drift between DO state and the live GitHub labels (the
DO is the source of truth; manual edits are healed).

The alarm self-rearms (every 60 minutes), bootstrapped from the first
event() call. applyAgentResult now clears currentRunId on completion so
stale-run detection doesn't false-positive on a finished run.
Adds createPullRequest and closePullRequest helpers to github.ts, and
splits the orchestrator's runAction into runInvestigate / runOpenPr /
runClosePr. PR number is persisted to DO storage on open so close knows
what to target.

openPr will fail in Phase 1 (no fix branch exists yet -- the investigate
workflow's git push step is Phase 2). The runError surfaces in the
EventOutcome; DO state still advances.
The orchestrator was passing input.labels straight through to resolve(),
even when DO storage held a newer state from a prior transition. A
follow-up event (status check, retry, etc.) carried stale labels from
the webhook's snapshot and the router decided against them.

Now: if DO storage has persisted state, project it to labels via
projectLabels() and resolve against those. Falls back to input.labels
for first-time mentions where DO storage is empty.
The workflow now runs in two trusted phases around the agent:

1. setupSandbox(): before the agent starts, mint an installation token,
   write it to /root/.git-credentials (persistent credential store), put
   it in /etc/environment as GITHUB_TOKEN, configure git identity, and
   clone the repo into /workspace/repo. For revise mode, check out the
   existing bot/fix-N branch.

2. detectPush(): after the agent reports done, query the GitHub API for
   the bot/fix-N branch. If it exists, set pushed:true in the callback.
   The router's outcomeFromResult uses pushed to gate agent.fix_ready
   (fixed:true without a real branch demotes to agent.failed).

The agent now defaults to kimi-k2.7-code (the code-focused model) and is
told via instructions that the repo and creds are ready.

No-creds dev mode skips clone and push detection silently; useful for
sandbox toolchain smoke tests.
Switches the credential path from 'agent sees the token' to 'agent never
sees the token'. The Sandbox subclass declares outboundByHost for
github.com / api.github.com / codeload.github.com pointing at
authenticatedGithub, which mints a fresh installation token in the
Worker runtime, injects Basic auth (x-access-token format that works for
both git smart HTTP and the REST API), and forwards upstream.

allowedHosts denies everything except github + the registry hosts the
toolchain may need (npm + githubusercontent CDNs). enableInternet=false
makes that the only path out.

The agent now pushes directly: git clone/fetch/push to github.com is
transparent and the sandbox holds no tokens. The SKILL.md says so
explicitly to discourage poking at /etc/environment etc.

Wired in the investigate skill via the with-skill import attribute. The
orchestrator's investigate workflow import is lazy so the workers-pool
test pipeline (which has no Flue build plugin) doesn't try to parse the
markdown.
…ed repo

Three bugs together prevented the sandbox setup from working:

1. interceptHttps defaults to false in @cloudflare/containers 0.3.x even
   though the April 2026 changelog says HTTPS interception is on by
   default. Override to true on our Sandbox subclass so outboundByHost
   handlers actually see github.com traffic (otherwise the proxy was
   blind to git clone over HTTPS).

2. harness.shell inherits the agent's cwd (/workspace/repo), which does
   not exist before the clone. Pass cwd: "/" so setup steps run from a
   directory that exists.

3. The setup script was a single .join(" && ") string containing an
   if/then/else/fi block, which is a bash syntax error ("if ... && else"
   never parses). Split into a typed steps[] array and run each via its
   own harness.shell call. Each step's exitCode is checked explicitly
   and stderr surfaces in the worker log on failure.

Adds x-emdash-dry-run: 1 header support: routes.ts forwards the flag
into NormalizedEvent.dryRun, and the orchestrator skips applySideEffects
(label flip + status comment) when dry-run. The workflow still runs in
full (LLM call, sandbox setup, push attempt). Lets local smoke tests
iterate without spamming labels/comments on a real GitHub issue.

Verified: dry-run repro against issue #1042. Setup steps all exit 0,
the agent's first command finds a populated /workspace/repo, the agent
runs real grep/find investigations against the cloned source.
Adds a final pnpm-install step to setupSandbox so the agent doesn't burn
turns rediscovering that it needs deps and figuring out which install
command works. --frozen-lockfile matches the lockfile in the checked-out
tree, which may differ between main and a revise branch.

The step is non-fatal: pnpm's post-install hooks can exit 1 in the
sandbox env (e.g. on missing build tools for native modules) while still
leaving node_modules populated enough to run tests. Failure is logged
with stdout/stderr tails but doesn't abort setup. Critical steps (clone,
checkout) remain fatal.

Bumps the install timeout to 10 min; clone gets 5 min. Updates SKILL.md
so the agent knows deps are pre-installed.
EmDash's pnpm-lock pins @lunariajs/core from pkg.pr.new; add it to the
Sandbox's allowedHosts so pnpm install can fetch it. Without this, the
install fails with ERR_PNPM_FETCH_520 (ContainerProxy returns 'Origin is
disallowed' status 520 for any host not in allowedHosts).

Add build-essential, python3, and python-is-python3 to the Dockerfile so
node-gyp can rebuild native modules (better-sqlite3, etc.). The previous
image only had ca-certificates + browser deps, which left node-gyp
unable to compile.

Also bumped the setup step failure log slice from 800 to 4000 chars so
node-gyp's actual error message is visible; the trailing summary alone
isn't enough to diagnose.
Replaces the bot-jargon transition comments ('Moved to working on retry
(investigate.repro).') with messages a reporter actually wants to read.
agent.fix_ready now includes a pkg.pr.new install URL (the existing
preview-releases.yml workflow auto-publishes for every push to
bot/fix-*), so reporters can pnpm-add the preview directly rather than
having to clone a branch. PR stays gated on .

Readonly status replies are state-specific now too instead of dumping
the verb list, and unmapped events get no comment at all (skip-post on
empty body).
Production sandbox network is slower than local docker; the 10 min limit
killed the install on a real run against #1623 before postinstall hooks
(better-sqlite3, sharp, workerd binaries) could finish. 15 min covers
the cold-cache case based on dev timings (~7 min) plus headroom.
- instance_type: standard-3 (2 vCPU, 8 GiB, 16 GB). The default lite (256
  MiB / 1/16 vCPU) was killing pnpm install on cold cache.
- NODE_OPTIONS=--max-old-space-size=6144 so node/pnpm processes can use
  the increased memory ceiling (mirrors what we bumped for emdash builds).
- Drop FlueSandboxProbeWorkflow. It was a Phase 0 toolchain probe that
  hasn't been invoked since; v2 migration deletes the DO class.
ripgrep, jq, tree, less, file, unzip, sqlite3. The agent fell back to
grep -rnH and similar in the first prod run because rg wasn't installed.
sqlite3 is useful when poking at D1/SQLite-backed tests.
A workflow can be evicted mid-run (DO hibernation, deploy rotation,
infra eviction) without producing a callback. The hourly tick already
dropped the orphaned currentRunId, but left DO state stuck on "working"
forever, with no way out except a manual reset. Synthesize an
agent.failed event after dropping the run so the state machine progresses
to `failed` and the reporter gets the failed-state comment.
Subscribes once at app startup to the Flue event stream and logs a
compact one-liner per run/turn/tool event. wrangler tail now shows what
the agent is thinking, not just the raw sandbox.exec commands -- much
easier to spot when the agent is stalled vs. mid-LLM-call.
The agent.reproduced event maps to working -> blocked; we already had
text for the other agent.* events but missed this one. The state flipped
silently on #1589, leaving the reporter with just a label change and no
explanation.
…maries

The outbound proxy now signs api.github.com requests scoped to the
current anchor issue/PR. The agent can curl GETs anywhere on
api.github.com, POST comments and reactions on its own issue, and push
to bot/fix-<n>; writes to other issues, PRs, or repos are denied 403.
Anchor + repo are passed via ctx.params, installed per-run by the
workflow before the agent session starts.

Replaces the boilerplate bot-jargon comments with the agent's own
summary. User-driven transitions (repro / implement / confirm /
decline / reopen / reset / take_over / hand_back) no longer post --
the user just typed the verb, echoing it is noise. Agent transitions
post the summary verbatim, with structural footers (pkg.pr.new install
URL for fix_ready; "reply with steps" prompt for not_reproduced; etc.).

SKILL.md tells the agent the summary IS the comment, and to write it
to the reporter.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 17, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache 0dea3f8 Jul 19 2026, 11:27 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 17, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-do 0dea3f8 Jul 19 2026, 11:27 AM

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 7,468 lines across 38 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
docs 4084d68 Jul 17 2026, 09:14 PM

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Migrates infra/emdash-bot from Flue 1 workflow-style orchestration to Flue 2 agents, making the Orchestrator Durable Object the canonical source of bot state and adding a Workers-pool integration test harness for DO + webhook ingress.

Changes:

  • Adds an OrchestratorDO implementation with durable webhook admission, inbox processing, dispatch recovery, and stale-run recovery.
  • Moves classification and investigation to Flue 2 agents and introduces a repository-scoped authenticated GitHub outbound proxy for the sandbox.
  • Adds Worker/Vite/Wrangler/Vitest configuration and a container image for the investigate sandbox.

Reviewed changes

Copilot reviewed 36 out of 38 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pnpm-workspace.yaml Pins Flue’s transitive hono version to match the bot’s direct dependency.
infra/emdash-bot/wrangler.test.jsonc Adds a test-only Wrangler config mirroring production bindings for workers-pool tests.
infra/emdash-bot/wrangler.jsonc Adds production Wrangler config including DO migrations, AI, R2, and required secrets.
infra/emdash-bot/vitest.workers.config.ts Configures Vitest to run integration tests in a real workerd isolate via workers-pool.
infra/emdash-bot/vitest.config.ts Adds a unit-test-only Vitest config for pure logic tests.
infra/emdash-bot/vite.config.ts Adds Vite config wiring Flue + Cloudflare Vite plugin for Worker builds.
infra/emdash-bot/tsconfig.json Adds TypeScript config for the bot package (workers-pool types, strict settings).
infra/emdash-bot/tests/unit/webhook.test.ts Adds unit coverage for webhook normalization and actor classification logic.
infra/emdash-bot/tests/unit/sandbox-deadline.test.ts Adds unit coverage for sandbox RPC deadline bounding helpers.
infra/emdash-bot/tests/unit/router.test.ts Adds unit coverage for the state-machine router logic and parsing rules.
infra/emdash-bot/tests/unit/github-proxy.test.ts Adds unit coverage for the sandbox GitHub proxy request gate.
infra/emdash-bot/tests/unit/classifier-client.test.ts Adds unit coverage for classifier result validation/mapping.
infra/emdash-bot/tests/integration/webhook.test.ts Adds end-to-end tests for webhook ingress + signature validation + DO admission in workerd.
infra/emdash-bot/tests/integration/orchestrator.test.ts Adds workerd integration tests for OrchestratorDO persistence, dedupe, recovery, and tick behavior.
infra/emdash-bot/tests/integration/_entry.ts Adds a workers-pool-only Worker entry that mounts core routes and exports DO classes.
infra/emdash-bot/package.json Introduces the bot package with Flue runtime, Hono, workers testing deps, and scripts.
infra/emdash-bot/entrypoint.sh Adds container entrypoint to trust Cloudflare’s outbound interception CA at runtime.
infra/emdash-bot/Dockerfile Adds a sandbox container image (Node/pnpm/git/etc.) for the investigate agent.
infra/emdash-bot/.gitignore Ignores build outputs and local Wrangler/env artifacts for the bot package.
infra/emdash-bot/.flue/wrangler-main.ts Adds a type-only main stub for wrangler types to generate correct bindings.
infra/emdash-bot/.flue/skills/investigate/instructions.md Adds the investigate agent’s operating instructions embedded into the skill.
infra/emdash-bot/.flue/routes.ts Adds core routes (health + GitHub webhook) separated for test entry mounting.
infra/emdash-bot/.flue/raw.d.ts Adds typing for ?raw imports used by skill documents.
infra/emdash-bot/.flue/lib/webhook.ts Adds webhook signature verification + payload normalization into durable DO events.
infra/emdash-bot/.flue/lib/sandbox-deadline.ts Adds deadline wrappers to bound sandbox RPC operations.
infra/emdash-bot/.flue/lib/router.ts Adds pure routing logic from machine state/labels + comment parsing and resolution.
infra/emdash-bot/.flue/lib/orchestrator.ts Adds the OrchestratorDO implementation: inbox, dedupe, side effects, dispatch + recovery.
infra/emdash-bot/.flue/lib/observer.ts Adds Flue event observer logging for more actionable production logs.
infra/emdash-bot/.flue/lib/machine.ts Adds the single-source-of-truth bot state machine (states/events/transitions/validation).
infra/emdash-bot/.flue/lib/github.ts Adds GitHub App helpers for token minting and repo-side effects (labels/comments/PR ops).
infra/emdash-bot/.flue/lib/github-proxy.ts Adds sandbox proxy gate to restrict GitHub/api access (repo-scoped, read-only API, bot-branch-only push).
infra/emdash-bot/.flue/lib/classifier-client.ts Adds classifier dispatch + validation/mapping of structured classifier outputs.
infra/emdash-bot/.flue/cloudflare.ts Adds Cloudflare-target DO exports and authenticated outbound GitHub proxy for the sandbox.
infra/emdash-bot/.flue/app.ts Adds Worker entry wiring core routes + authenticated Flue agent control routes.
infra/emdash-bot/.flue/agents/investigate.ts Adds Flue 2 investigate agent integrating sandbox setup + result reporting to the DO.
infra/emdash-bot/.flue/agents/classify-command.ts Adds Flue 2 classifier agent for choosing a safe command from a bounded set.
infra/emdash-bot/.env.example Adds local dev secret template for webhook secret and app private key.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread infra/emdash-bot/.flue/lib/router.ts Outdated
// `@emdashbot decline\nplease don't` would otherwise be treated as a bare
// `decline` (an `$` end-of-line anchor with the `m` flag matches after the
// verb word), bypassing the destructive-event guard.
const MENTION_RE = /^[ \t]*@emdashbot[ \t]+([\s\S]*)/m;
Comment on lines +355 to +366
let machineEvent: NormalizedEvent["event"];
switch (action) {
case "opened":
case "reopened":
machineEvent = "pr.opened";
break;
case "closed":
machineEvent = "pr.merged"; // refined to merged-vs-closed via pr.merged below
break;
default:
return { kind: "skip", reason: `pull_request.${action} not handled` };
}
Comment on lines +9 to +12
- `curl https://api.github.com/...` GET anything (read issues, PRs, files, blobs).
- `curl -X POST https://api.github.com/repos/emdash-cms/emdash/issues/<your-issue>/comments -d '{"body":"..."}'` to post a comment on the issue you're working on.
- Same for `/reactions` to add an emoji reaction.
- Writes outside the configured repository are denied (403). Don't try.
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/needs-review No maintainer or bot review yet labels Jul 17, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jul 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/admin@2101

@emdash-cms/auth

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/auth@2101

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/auth-atproto@2101

@emdash-cms/blocks

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/blocks@2101

@emdash-cms/cloudflare

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/cloudflare@2101

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/contentful-to-portable-text@2101

emdash

npm i https://pkg.pr.new/emdash-cms/emdash@2101

create-emdash

npm i https://pkg.pr.new/emdash-cms/emdash/create-emdash@2101

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/gutenberg-to-portable-text@2101

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-cli@2101

@emdash-cms/plugin-types

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-types@2101

@emdash-cms/registry-client

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-client@2101

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-lexicons@2101

@emdash-cms/registry-verification

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-verification@2101

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/sandbox-workerd@2101

@emdash-cms/x402

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/x402@2101

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-ai-moderation@2101

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-atproto@2101

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-audit-log@2101

@emdash-cms/plugin-color

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-color@2101

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-embeds@2101

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-field-kit@2101

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-forms@2101

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-webhook-notifier@2101

commit: 0dea3f8

@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 17, 2026
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 17, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 18, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right change for the stated goal: moving emdashbot orchestration from Flue 1 workflows into Flue 2 agents and making the per-issue Durable Object the canonical source of state. The scope is well-contained to infra/emdash-bot/, the state machine/router separation is clean, and the test matrix (unit router/proxy tests + workers-pool DO/integration tests) covers the most failure-prone seams.

I reviewed the diff statically against the checked-out PR head. I did not run the test/lint/build tooling (no shell in this environment), so test results are taken as unverified claims from the PR description.

Headline conclusion: the architecture is sound, but there are a few concrete issues that should be fixed before merging:

  1. Sandbox GitHub proxy auth scheme is wrong for API calls. The proxy sends Basic x-access-token:<token> to every host. That works for git HTTPS traffic on github.com, but the GitHub REST API expects Authorization: Bearer <installation_token>; Basic will 401 for API reads the agent may perform from inside the container.
  2. Readonly status replies do not respect x-emdash-dry-run: 1. The transition path correctly suppresses side effects when dryRun is set, but the readonly branch still queues a GitHub comment, defeating the smoke-test mode.
  3. Type generation is missing for a clean checkout. tsconfig.json includes worker-configuration.d.ts, which is gitignored and not regenerated by any package script, so pnpm typecheck will fail on a fresh clone until someone manually runs wrangler types.
  4. pull_request.closed is collapsed into pr.merged. Closing a bot PR without merging will still move the anchor issue to done. There is a comment acknowledging this, but it is a real state-machine regression that should be handled before the rewrite goes live.
  5. pkg.pr.new preview URL in the fix-ready comment looks incorrect. It uses the unqualified emdash@<branch> form; for this repo the canonical URL should include the full owner/repo@<branch> path.

Addressing (1) and (2) are the most important; (3) is a contributor-experience issue; (4)-(5) are follow-up polish.

Comment thread infra/emdash-bot/.flue/cloudflare.ts Outdated
return new Response("token mint failed", { status: 502 });
}
const authed = new Request(request);
authed.headers.set("authorization", `Basic ${btoa(`x-access-token:${token}`)}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] The outbound GitHub proxy uses Basic x-access-token:<token> for every host. This scheme is correct for git HTTPS operations on github.com, but the GitHub REST API requires Authorization: Bearer <installation_token>; Basic auth on api.github.com will be rejected. Any API read the agent performs via curl inside the sandbox will fail.

Suggested change
authed.headers.set("authorization", `Basic ${btoa(`x-access-token:${token}`)}`);
const isApi = url.host === "api.github.com";
authed.headers.set(
"authorization",
isApi ? `Bearer ${token}` : `Basic ${btoa(`x-access-token:${token}`)}`,
);

return { kind: "noop", reason: decision.reason };
}
if (decision.kind === "readonly") {
await this.postReadonlyReply(decision, input);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] The readonly/status reply branch enqueues a GitHub comment without checking input.dryRun. The transition path already suppresses label/comment side effects when dryRun is true, so a dry-run @emdashbot status still mutates the issue. Make the readonly path consistent.

Suggested change
await this.postReadonlyReply(decision, input);
if (decision.kind === "readonly") {
if (!input.dryRun) await this.postReadonlyReply(decision, input);
if (input.deliveryId) await this.recordDelivery(input.deliveryId);
return { kind: "readonly", state: decision.state, event: decision.event };
}

"skipLibCheck": true,
"noEmit": true
},
"include": [".flue/**/*.ts", "tests/**/*.ts", "worker-configuration.d.ts"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] tsconfig.json depends on worker-configuration.d.ts, but that file is gitignored and there is no pretypecheck script (or equivalent) that runs wrangler types. A fresh clone will hit a missing-file error on pnpm typecheck. Add a generation step or commit a generated stub.

case "reopened":
machineEvent = "pr.opened";
break;
case "closed":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] pull_request.closed always emits pr.merged, so a bot PR closed without merging will still move the anchor issue to done. The code already notes this as a follow-up, but it is a real state-machine regression in the new rewrite. Read payload.pull_request.merged and emit pr.closed when the PR was not merged.

"Try it:",
"",
"```sh",
`pnpm add https://pkg.pr.new/emdash@bot/fix-${anchorNumber}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The fix-ready comment uses https://pkg.pr.new/emdash@bot/fix-${anchorNumber}, which omits the owner and is unlikely to be the canonical preview URL for this repo. Use https://pkg.pr.new/emdash-cms/emdash@bot/fix-${anchorNumber} so the generated link actually resolves.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 18, 2026
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-rereview Author pushed changes since the last review labels Jul 18, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jul 18, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 18, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This remains the right change: moving emdashbot orchestration from Flue 1 workflows into Flue 2 agents, with the per-issue Durable Object as the canonical state source. The scope is well-contained to infra/emdash-bot/, the separation between pure router logic and the stateful DO is clean, and the test matrix covers the main failure modes.

All five issues from the previous review have been addressed in the current head:

  1. GitHub proxy auth scheme now sends Bearer for api.github.com and Basic x-access-token for git hosts.
  2. x-emdash-dry-run: 1 now suppresses readonly status replies via shouldPostReadonlyReply.
  3. Type generation on a clean checkout is covered by the new pretypecheck script.
  4. pull_request.closed without merging now routes to blocked, not done.
  5. pkg.pr.new URL uses the canonical emdash-cms/emdash@… form.

I reviewed the diff statically against the checked-out PR head and did not re-run the test/lint/build tooling (no shell in this environment), so those test-result claims are taken as unverified.

One real logic bug remains: agent runs that were dispatched in implement or revise mode can never reach agent.fix_ready because outcomeFromResult gates every successful outcome behind reproduced === true. The skill instructions explicitly say reproduced is only meaningful in repro mode, and the result schema makes it optional, so @emdashbot implement <directive> runs that successfully push a fix currently land in blocked instead of awaiting_feedback. This should be fixed before the rewrite goes live, ideally by threading the run mode into outcomeFromResult so the reproduced check only applies to bug-reproduction runs. Coverage for the implement/revise success path is also missing.

A smaller suggestion: wrangler.test.jsonc intentionally warns against drift with production, but its compatibility_date lags wrangler.jsonc (2026-04-01 vs 2026-07-17), so the workers-pool tests may not exercise the same runtime surface as production.

Comment thread infra/emdash-bot/.flue/lib/router.ts Outdated
Comment on lines +339 to +351
export function outcomeFromResult({
ok,
result,
pushed,
}: {
ok: boolean;
result?: AgentResult | null;
pushed?: boolean;
}): EventId {
if (!ok || !result || typeof result !== "object") return "agent.failed";
if (result.skipped === true) return "agent.skipped";
if (result.verdict === "intended-behavior") return "agent.by_design";
if (result.reproduced !== true) return "agent.not_reproduced";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] outcomeFromResult requires reproduced === true before it will ever emit agent.fix_ready, which is wrong for implement and revise runs.

  • In investigate.ts the skill instructions say reproduced is "only meaningful in repro mode".
  • The result schema makes reproduced optional.
  • But this function returns agent.not_reproduced whenever reproduced is not true, even if fixed is true and the branch was pushed.

So a successful implement or revise run can never advance the issue to awaiting_feedback; it will get stuck in blocked. The clean fix is to thread the run mode through to this function and only gate on reproduced for mode === "repro".

export function outcomeFromResult({
	ok,
	result,
	pushed,
	mode, // <-- plumbed from the DO's current run
}: {
	ok: boolean;
	result?: AgentResult | null;
	pushed?: boolean;
	mode?: "repro" | "implement" | "revise";
}): EventId {
	if (!ok || !result || typeof result !== "object") return "agent.failed";
	if (result.skipped === true) return "agent.skipped";
	if (result.verdict === "intended-behavior") return "agent.by_design";
	if (mode === "repro" && result.reproduced !== true) return "agent.not_reproduced";
	if (result.fixed === true) return pushed === true ? "agent.fix_ready" : "agent.failed";
	return mode === "repro" ? "agent.reproduced" : "agent.failed";
}

Store currentRunMode in the DO when dispatching (prepareInvestigation already knows the mode), pass it into applyAgentResult, and add unit/router tests for implement/revise outcomes and an integration test where such a run yields fix_ready.

"$schema": "node_modules/wrangler/config-schema.json",
"name": "emdash-bot-test",
"account_id": "1f74638c495bc9f0330ce5c8e64c1b6b",
"compatibility_date": "2026-04-01",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The file explicitly warns against drift with production, but the test worker is pinned to 2026-04-01 while wrangler.jsonc uses 2026-07-17. Aligning the compatibility_date (and any new flags) keeps the workers-pool tests from passing on a different runtime surface than production.

Suggested change
"compatibility_date": "2026-04-01",
"compatibility_date": "2026-07-17",

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 18, 2026
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-rereview Author pushed changes since the last review labels Jul 18, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla: signed review/needs-rereview Author pushed changes since the last review size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants