The sandbox egress allowlist only passes GitHub/npm hosts, so agent-browser's
runtime Chrome download can never succeed inside a container; the browser has
to ship in the image. Install it in the node-source stage (working CA bundle,
same pattern as the Node copy) and copy the cache across. Verified with
agent-browser doctor's live launch test plus an open/get-text round-trip in
the built image.
* feat(emdash-bot): investigation eval harness + pre-fix ref checkout
Operator tool (pnpm evals) that drives the deployed worker's investigate
agent against 26 curated closed issues and scores each verdict against
recorded ground truth. Gate: zero confident-wrong (a reproduction asserted
on a case with nothing to reproduce). Cannot run in CI; the dataset loader,
pre-fix SHA resolution, scorer, and formatter are unit-tested.
Adds an optional baseRef to the investigate agent so a run can stand up at a
fixing PR's pre-fix commit; cloneRepo and the container checkout now resolve a
branch, tag, or bare commit SHA uniformly.
* chore(emdash-bot): cut over to the worker; retire gen-1 Actions bot
Delete the gen-1 .flue/ workspace and four issue-automation workflows
(investigate, reporter-reply, maintainer-reply, bot-cleanup) now that the
emdash-bot worker owns issue-side automation. The repro/diagnose/verify skill
prompts were already ported into the worker. Clean the dead investigate.yml
entry from zizmor.yml and a stale reference in a preview-releases.yml comment
(comment only; the workflow is independent and untouched).
Keep triage-project-sync.yml: board sync needs org-level Projects v2 write and
keys off the retired triage/* labels, so it is a later singleton, not a cutover
change -- documented in evals/CUTOVER.md. Rewrite TRIAGE.md's bot sections for
the maintainer-directive flow, the reporter preview-confirm loop, and the new
bot:* state labels. evals/CUTOVER.md drafts the cutover PR body and holds the
eval-gate section to fill after the live pre-flight run.
* chore(emdash-bot): drop dead labelTriggers; correct drift-reconcile comment
Nothing reads EventMeta.labelTriggers and the webhook skips labeled/unlabeled
events, so no bot:* label fires any event -- comments are the only entry. Remove
the labelTriggers from all four events (repro/investigate/implement/fix) and the
now-orphaned field, and regenerate machine.json. Correct the normalizeIssues
comment: label drift is reconciled by the Orchestrator DO's periodic alarm tick
(reconcileLabels), not a cron -- the worker has no cron trigger.
* style: format
* fix(emdash-bot): quote dynamic values in container setup commands
* chore(emdash-bot): drop stale workflow references from machine.ts comments
* docs(emdash-bot): match TRIAGE.md to the mention-only trigger; drop deleted-workflow references
* fix(emdash-bot): drop the experimental compatibility flag; deployed workers reject it and the worker loader runs without it
* fix(emdash-bot): export WorkspaceServiceProxy so the worker-shell loopback resolves
* fix(emdash-bot): install @platformatic/vfs for computer's in-VFS git
* fix(emdash-bot): hydrate the VFS from the GitHub tarball; in-VFS git clone exceeds DO memory
* feat(emdash-bot): move the isolate VFS to @cloudflare/shell with a durable change log
Replaces @cloudflare/computer, whose SQLite provider cannot serve a
repo-sized tree in a deployed DO. The agent's own DO hosts a
@cloudflare/shell Workspace (R2 spill); container materialization replays
a durable write log instead of parsing git status; the code tool is a
read-only analysis surface; git and the toolchain live in the container.
* fix(emdash-bot): degrade gracefully when no usable App credential exists
* fix(emdash-bot): cap tool results and harden reporting discipline
Unbounded tool output burst kimi's context window on long
investigations (five eval errors); every tool result now truncates at
24KB with a narrowing hint, and the code tool describes its read-only
surface compactly instead of embedding the full state typings.
report_result and the investigate skill now require a demonstrated
failure for reproduced=true and route missing-information cases to
verdict=unclear (needs_info).
* fix(emdash-bot): give read_file a line range, raise the result cap, and demand the demonstration attempt
* fix(emdash-bot): reproduced means the reported issue; adjacent findings and infra symptoms report as findings
* fix(emdash-bot): move reproduction discipline into the result schema
The report schema now carries demonstration (how the failure was shown)
and demonstratedReportedIssue (whether it is the reporter's defect), and
rejects reproduced=true without both -- the distinction prose tuning
kept missing in both directions. A faithful-path demonstration counts:
a failing unit test reproduces a UI-reported defect.
* feat(emdash-bot): first-class diagnosed verdict for root causes without a confirming repro
rootCauseFound on the agent result maps to a new agent.diagnosed event
and bot:diagnosed state, actionable like reproduced (the fix loop still
verifies with a failing test first). The eval scorer grades it
separately from pass and miss; a diagnosed claim on a negative case is
a miss, not confident-wrong.
* style: format
* chore: sync lockfile with emdash-bot dependency swap
* docs: record run-7 eval gate result in CUTOVER.md
* style: format
* feat(evals): relabel #1193 as an unfixed confirmed bug; sharpen skill verdict discipline
The agent's run-7 reproduction of #1193 (update() overwrites published_at)
was independently confirmed; the NOT_REPRODUCIBLE label was wrong. Unfixed
confirmed bugs (fixing_pr: null) now run at main with no pre_fix.
Skill: needs-info reports settle before standing up the toolchain (run-7
#1113 burned its 30-minute budget building against an underspecified
report), and rootCauseFound requires a located code defect, not an
explanation (negatives were landing diagnosed instead of not_reproduced).
* docs: record run-8 eval gate result
* feat(evals): relabel #1022 as an unfixed confirmed bug; gate passes
Maintainer adjudication: the agent's asserted reproduction (legacy seo data
rejected as an unknown field by validateContentData) is a real, unfixed bug;
the NOT_REPRODUCIBLE label was wrong. Run 8's stored verdicts re-scored
against the corrected dataset read GATE PASSED (16 pass, 1 diagnosed, 9 miss,
0 confident-wrong, 0 errors); CUTOVER.md records the result.
---------
Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
* fix(flue-review): elide oversized diff sections before staging for the model
* fix(flue-review): mark unreducible diff sections so the budget loop terminates
* feat(emdash-bot): pkg.pr.new preview helpers, ask/PR renderers, artifacts push
Reader-side of the fix loop's preview build. `preview.ts` centralises the
full-ref pkg.pr.new install URL (which the poll probes and the ask advertises),
the fix/artifacts branch names, a bounded readiness probe, and the reap
branch-set. `comments.ts` gains the ask renderer (bot-ask marker, install
command, screenshots served from the artifacts branch with filename validation
and markdown escaping, reporter ask) and the confirmed draft-PR body.
The outbound push proxy now admits `bot/artifacts-<n>` alongside `bot/fix-<n>`
for the issue, and the fix agent reports screenshots and pushes the orphan
artifacts branch so the ask can render them.
* feat(emdash-bot): fix-loop preview polling, ask, reap guard, issue-close cleanup
The orchestrator now drives the fix loop end to end. On entering
preview_building it schedules a bounded pkg.pr.new poll on the DO alarm (one
probe per tick, ~10min budget); a 200 fires preview.ready, exhaustion fires
preview.failed back to the reproduced verdict. preview.ready composes the ask
from the persisted fix notes, screenshots, and the reporter login and posts it
before flipping labels, so a failed comment can't strand the issue in
awaiting-reporter with no ask.
Branch reaping now guards the fix branch behind an open-PR check and always
drops the artifacts branch (review flag F4), on the reject/expire/decline edges
and a new issues.closed cleanup path that bypasses the machine.
Also fixes a latent drain bug: a standalone side effect (runId undefined) with
no dispatch pending matched `undefined === pendingDispatch?.runId` and was
deferred forever. The poll-fired ask is the first non-dry-run standalone effect
to exercise it.
* style: format
* chore(emdash-bot): type the fetch mock via Parameters<typeof fetch> for typeless CI lint
---------
Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
* feat(emdash-bot): add execEnv seam over computer 0.1.1 + sandbox
Thin, testable seam wrapping the two investigation substrates: computer
0.1.1 Workspace (isolate/fs/git) and @cloudflare/sandbox (container). Routes
exec by target, bounds every op with a deadline, bridges VFS edits into the
container checkout, and confines every @cloudflare/computer touchpoint to one
module so the future computerd flip is a single-adapter change.
* refactor(emdash-bot): run execEnv over the getWorkspace RPC client
emdash is public, so the VFS read-clone needs no auth and runs via an
isolate git command; fs+runtime reach the DO through their RPC stubs, so the
seam lives agent-side. Drops the in-DO typed-git dependency and keeps token
minting confined to the container push path.
* feat(emdash-bot): land investigation skills as Flue 2 skill directories
Ports the repro-public/repro-admin/repro-api/diagnose/verify/fix leaf skills
and the investigate spine from the gen-1 .flue/skills content onto computer's
tool model, as flat SKILL.md directories the agent loads per mode. Replaces the
single investigate/instructions.md.
* feat(emdash-bot): wire investigate agent onto the hybrid execEnv
Adds the WorkspaceDO (computer Workspace + worker-shell isolate backend via a
LOADER binding) and swaps the investigate agent off useSandbox onto execEnv:
read/write/edit/ls/grep + an exec tool the model routes isolate-vs-container,
skills loaded per mode, VFS clone at setup. Container stays @cloudflare/sandbox.
Bindings: experimental flag, worker_loaders, WorkspaceDO DO + append-only v4
migration. Build + 107 unit + 35 integration green; typecheck at baseline.
* refactor(emdash-bot): drop the dead useSandbox deadline wrapper
withSandboxDeadlines wrapped Flue's useSandbox SessionEnv, which the execEnv
swap removed. withDeadline (still used by the classifier, orchestrator, and
execEnv) stays.
* fix(emdash-bot): make the VFS->container bridge stateless (review F1/F4/F9)
F1: derive the container sync set from the VFS itself (git status against the
checkout) before every container exec, instead of tracking dirty paths in
memory. Materializing before each exec -- not only at attach -- closes the
edit-after-attach gap (stale container source, and a fix push that omits
post-attach edits) and survives an isolate resume, since the VFS is the only
source consulted. Removes #dirtyPaths and the write-through path; the one-time
base checkout stays in the injected attachContainer.
F4: readArtifact takes a bare filename under .bot-artifacts/ -- rejects path
separators, '.'/'..', absolute forms, and a symlinked target.
F9: rewrite the module header to operative facts; the coherence claim is now
true because every container exec re-derives from the VFS.
* fix(emdash-bot): parse git status via NUL-delimited -z (review delta)
Non-z porcelain C-escapes and quotes special-character paths, so a delete of
such a path silently no-opped in the container (surviving stale file), and the
' -> ' rename heuristic misfired on paths containing that literal. Switch to
'git status --porcelain -z': NUL-delimited, never quoted or escaped, with
rename/copy old paths in a trailing NUL field (new-then-old). Adds rename and
non-ASCII regression tests.
* fix(emdash-bot): retry workspace client after a failed connection and mirror WorkspaceDO bindings in the test config
* fix(emdash-bot): idempotent cloneRepo, default exec target, diagnose-gated push capability; align skill docs with agent-owned pushes
* fix(emdash-bot): probe clone usability before reuse and give repro runs the fix skill
* chore(emdash-bot): widen the storage assertion lint directive for typeless CI lint
* chore(emdash-bot): regenerate worker types for the WorkspaceDO and LOADER bindings
* style: format
* feat(emdash-bot): add investigation + fix-loop states to the machine
Adds the maintainer-triggered investigation lane (reproduce + diagnose,
no fix) and the fix loop (candidate branch -> preview build -> reporter
confirm -> draft PR, else reap): seven new states, the preview.* and
expire event families, and the diagnose/fix/openDraftPr/reapBranch
actions. Fully additive -- every existing state, event, and transition is
preserved. validateMachine passes: 17 states, 30 events, 87 transitions.
* feat(emdash-bot): wire investigation + fix-loop consumers
Threads the new machine states through the runtime:
- comments: readonly replies for the seven states, a needs_info agent
comment, and target-aware CTAs for agent.reproduced/fix_ready so the
verdict and preview lanes read differently from the legacy lanes.
- router: diagnose and fix modes in outcomeFromResult (verdict unclear ->
needs_info; fix advances only when pushed).
- orchestrator: parseInvestigateMode learns diagnose/fix; runAction
dispatches openDraftPr (draft PR) and reapBranch (delete bot/fix-<n>);
a 14-day reporter-silence alarm fires expire.
- github: createPullRequest gains a draft option; adds deleteBranch.
- investigate agent: diagnose/fix modes plumbed, with diagnose-mode
prompt guidance that forbids a fix.
* build(emdash-bot): generate and drift-check machine artifacts
The machine.ts header promised generated artifacts that did not exist.
Adds `pnpm bot:generate` (scripts/generate-machine.ts) which renders
machine.json (runtime snapshot) and BOT_STATE_MACHINE.md (states/events/
transitions tables + mermaid diagram) deterministically from machine.ts,
plus a vitest drift check that fails if the committed artifacts fall out
of sync. The generator uses explicit .ts imports so node runs it without a
bundler; allowImportingTsExtensions keeps tsc happy. The artifacts are
excluded from oxfmt so the generator owns their format.
* test(emdash-bot): cover investigation + fix-loop paths
Router unit tests: maintainer-only authority on investigate/fix, the
investigation verdicts, the fix loop through preview to the reporter wait,
confirm/reject/expire from awaiting_reporter, and outcomeFromResult in
diagnose/fix modes. Orchestrator integration tests drive the real DO and
machine through the happy fix loop (diagnosis -> fixing -> preview_building
-> awaiting_reporter -> confirmed draft PR), the reject and 14-day expire
reaps, needs_info from verdict unclear, and non-maintainer rejection.
* fix(emdash-bot): route a skipped fix run out of fixing
A fix-mode run that reports skipped emitted agent.skipped, which fixing
had no transition for -- the event no-oped and the run was cleared, so the
stale-run watchdog (which only covers runs that hang without reporting)
never fired. The item wedged in fixing permanently, escapable only by an
unadvertised bare reset. Adds fixing --agent.skipped--> blocked, mirroring
investigating, so every reportable outcome of a fix run is covered.
Regenerates the artifacts.
* test(emdash-bot): enforce validateMachine in the generator and drift tests
* style: format
* fix(emdash-bot): withhold the push capability from diagnose-mode sandboxes; typecheck the generator script
* chore(emdash-bot): run the generator with type stripping; refresh stale orchestrator and machine comments
---------
Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
* chore(emdash-bot): bump @flue/runtime + @flue/vite to 2.0.3
Move the bot off the reviewed Flue 2 nightly onto the 2.0.3 release, and
align its bundled agents SDK dep with what @flue/vite ships (0.20.1). The
workspace hono override and cooldown-exclude pins are updated to match; the
nightly excludes are dropped since 2.0.3 is past the release-age cooldown.
* refactor(emdash-bot): port agent/tool/sandbox code to Flue 2 APIs
Flue 2.0.3 tightens several surfaces the bot uses:
- defineTool run() with an output schema must return { output } (envelope),
not the bare value.
- init() handles split dispatch (returns a receipt) from read() (awaits the
reply); the classifier now dispatches then reads with its timeout signal.
- DispatchReceipt.dispatchId is renamed submissionId.
- FlueEvent no longer carries dispatchId (observer drops it).
- SandboxFactory's primary method is createSandbox; createSessionEnv is
deprecated, so withSandboxDeadlines must wrap createSandbox or the
deadlines are silently bypassed. The unit test's mock follows suit.
Behavior is unchanged; these are mechanical adaptations to the new types.
* feat(emdash-bot): enable Workers traces
Flue 2's generated Worker entry installs content-on Cloudflare tracing by
default; this flag activates collection on the account. Content is enabled
(issues/PRs are public), matching the design's diagnosable-by-default goal.
* test(emdash-bot): raise workers-pool testTimeout for AI-binding warmup
The AI binding forces the vitest workers pool onto a remote proxy; the first
test to run pays the connection warmup and intermittently overran the 5s
default. Raise to 20s so the suite is deterministic.
* feat(emdash-bot): install explicit content-on Cloudflare tracing in app entry
Register instrument(createCloudflareTracing()) at module scope so content
capture is explicit rather than relying on Flue's default install. Content is
on (public issue/PR data); the API expresses on/off as omit/false, with no
true literal.
* fix(emdash-bot): bound classifier dispatch admission within the timeout budget
The dispatch/read split left handle.dispatch() (which resolves only on
admission) with no deadline, so an admission stall would hang classifyComment
past GitHub's webhook ack window instead of erroring. Wrap dispatch in
withDeadline and share one budget with the read so admission + settlement stay
within the 10s envelope.
Also trims narrative from the traces comment in wrangler.jsonc.
* test(emdash-bot): cover classifyComment dispatch path and fail read deterministically on an exhausted budget
* fix(emdash-bot): use a monotonic clock for the classifier budget; trim rationale comments
* chore(emdash-bot): commit generated worker types like the other worker packages
* fix(flue-review): cap tar entry size and honor pax linkpath overrides
* fix(flue-review): satisfy noUncheckedIndexedAccess in R2 instrumentation
* fix(flue-review): require a strictly octal tar size field
* fix(flue-review): hydrate from the GitHub tarball instead of JS git
isomorphic-git pack indexing in the workflow DO stopped completing once
the repo's shallow pack passed ~16MB, silently stalling every review in
the hydrating stage until the watchdog killed it. The tarball of the PR
head SHA needs no git objects at all: stream it through the runtime's
native gzip DecompressionStream and untar into the workspace (ustar +
GNU longname + pax paths, symlinks included, archive root stripped).
The diff was already API-fetched; nothing downstream used the git repo.
Also keeps the hydration stage/R2 instrumentation added while
diagnosing: every stage and R2 operation logs start/end so the next
stall identifies itself.
* style: format
* fix(flue-review): harden untar path handling and extract it for testing
Review findings on the tarball hydration: the parser is the boundary
between an externally-supplied archive and the workspace, so entry
paths and symlink targets now reject absolute paths and any traversal
that would resolve outside the destination. The parser moves to
lib/untar.ts behind a narrow target interface with a vitest suite
covering files, directories, symlinks, GNU longname, pax paths,
truncation, and the escape rejections. Stale JS-git and incident
narrative trimmed from comments.
---------
Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
* fix(ui): move Comments/CommentForm to emdash/ui/comments subpath (#2039)
Their <style> blocks were pulled into a shared, render-blocking CSS chunk
on every page that imported anything from the emdash/ui barrel (e.g.
PortableText), because Astro scans the whole barrel module graph. Splitting
them into a dedicated entry point keeps comment CSS off pages that don't
render comments, mirroring the existing emdash/ui/search entry point.
* fix(ui): keep barrel Comments exports as deprecated (#2039)
Address ascorbic review: add emdash/ui/comments without breaking existing
emdash/ui imports. Mark barrel re-exports @deprecated (remove in 1.0),
allowlist the subpath in the public-source guard, and sync template skills.
* infra/emdash-bot: scaffold Cloudflare-target Flue app (Phase 0 spike)
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.
* infra/emdash-bot: Phase 0 spike validated end-to-end
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.
* infra/emdash-bot: browser stack works in the Sandbox container
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.
* infra/emdash-bot: port state machine + router + 32 unit tests
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.
* infra/emdash-bot: OrchestratorDO skeleton + workers-pool test rig
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.
* infra/emdash-bot: webhook ingress -> Orchestrator DO
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.
* infra/emdash-bot: GitHub side effects in OrchestratorDO
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.
* infra/emdash-bot: wire classifier into OrchestratorDO via ctx.exports
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).
* infra/emdash-bot: invoke investigate workflow on action transitions
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).
* infra/emdash-bot: post a status comment for readonly events
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.
* infra/emdash-bot: cron tick via DO alarm
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.
* infra/emdash-bot: wire openPr / closePr actions
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.
* infra/emdash-bot: use DO state as source of truth in resolve()
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.
* infra/emdash-bot: pre-clone repo + detect push in investigate workflow
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.
* infra/emdash-bot: outbound-proxy github auth + investigate skill
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.
* infra/emdash-bot: fix Phase 2 sandbox setup; agent now sees a populated 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.
* infra/emdash-bot: pnpm install in setupSandbox
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.
* infra/emdash-bot: allow pkg.pr.new + add native build toolchain
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.
* infra/emdash-bot: human-readable bot replies
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).
* infra/emdash-bot: bump pnpm-install timeout to 15 min
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.
* infra/emdash-bot: beefier sandbox + drop unused probe DO
- 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.
* infra/emdash-bot: add investigation tools to the sandbox
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.
* infra/emdash-bot: advance to failed when tick drops a stale run
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.
* infra/emdash-bot: log agent turns via Flue observe()
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.
* infra/emdash-bot: comment on agent.reproduced transitions
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.
* infra/emdash-bot: scoped GitHub API for agent; comments are agent summaries
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.
* feat(emdash-bot): migrate to Flue 2 agents
* fix(emdash-bot): pin Flue Hono resolution
* chore(emdash-bot): remove durability spike
* refactor(emdash-bot): harden type boundaries
* fix(emdash-bot): allow reviewed Flue nightlies
* fix(emdash-bot): address Flue 2 review feedback
* fix(emdash-bot): preserve investigation mode
* chore: add empty changeset
Move the PR review bot from kimi-k2.7-code to Workers AI's
@cf/zai-org/glm-5.2 and cap its reasoning effort at "low" so it stops
over-deliberating on straightforward diffs. Bump the per-attempt review
timeout from 20 to 30 minutes.
GLM-5.2 only carries the metadata flue needs (reasoning support, context
window) in pi-ai 0.80.x, which requires @flue/runtime and @flue/cli
1.0.0-beta.9. That release removed the FlueContext/createAgent<Payload,Env>
workflow model, so review.ts is migrated to defineAgent + defineWorkflow:
the agent holds the model, thinkingLevel, and cf-shell sandbox, while the
Action's run performs PR hydration and GitHub posting, reading bindings
back through getCloudflareContext() and keying the Workspace on the
per-run Durable Object id.
* ci: release
* format
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Kane <m@mk.gg>
The monorepo's pnpm install peaks above Node's default ~2 GB old-space
heap on the 8 GB Workers Builds container, so installs OOM
nondeterministically during deploys. Set NODE_OPTIONS=--max-old-space-size=6144
as a build var (wrangler vars are injected into the build environment)
on the demo and infra Workers that deploy via Workers Builds. No runtime
effect; workerd ignores NODE_OPTIONS.
* fix(flue-review): re-architect reviewer onto cf-shell, drop sandbox container
The @cloudflare/sandbox firecracker container was the source of the
reviewer's reliability problems: ~53% of runs errored (OOM at
256MiB/0.0625vCPU, persistent-bash-session death on malformed model
commands, per-command Worker->DO RPC stat-storms, and a wide
DO-eviction window from slow container hydration).
The review agents are read-only (search/read files, no installs, tests,
or builds), so the container is overkill. Move hydration to the
Worker-Loader-backed cf-shell sandbox: clone the base + fetch the PR
head via isomorphic-git into a Workspace whose git packfile spills to
R2, and expose a single `code` tool over state.* (searchFiles /
readFile / glob) to the agent. The base...head unified diff is staged
via the GitHub API (cf-shell git only exposes a changed-files list, not
a two-ref content diff).
Removes the Sandbox DO, Dockerfile, and @cloudflare/sandbox; adds the
worker_loaders (LOADER) and R2 (REVIEW_WORKSPACE) bindings plus the
v5-remove-sandbox migration. Validated end-to-end locally on PR #1484
(verdict produced, review posted, no container failure modes).
* style: format
* fix(flue-review): satisfy type-aware lint in webhook handler and code tool
Replace unsafe `as` narrowings from `unknown` (status query param, run
payload, code-tool params) with runtime guards, and avoid base-to-string
on unknown values when formatting run errors and unserializable code-tool
results. No behavior change.
---------
Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
* ci: release
* format
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Kane <m@mk.gg>
* chore: upgrade to Astro 7
Bump the catalog to stable Astro 7 (astro 7.0.0, @astrojs/cloudflare 14.0.0,
@astrojs/node 11.0.0, @astrojs/react 6.0.0) and update templates, demos, and
packages for compatibility.
- Move route caching config from experimental.cache/routeRules to the stable
top-level cache/routeRules keys (demos/cloudflare, infra/cache-demo).
- Drop infra/cache-demo's obsolete pkg.pr.new Astro preview pin in favor of the
catalog; align its wrangler with the catalog (v14 adapter needs >=4.99.0).
- Externalize the bare zod import used by the type generator so EmDash sites
build on Cloudflare under Astro 7.
- Remove the now-obsolete @astrojs/telemetry override and refresh the
minimumReleaseAge cooldown exclusion for the Astro 7 release.
- Update @emdash-cms/cloudflare cache JSDoc to the stable top-level cache key.
* ci: update query-count snapshots
---------
Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>