feat(emdash-bot): maintainer-triggered investigation + fix-loop states (bot next-gen 2/5) - #2376
feat(emdash-bot): maintainer-triggered investigation + fix-loop states (bot next-gen 2/5)#2376ascorbic wants to merge 5 commits into
Conversation
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.
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.
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.
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.
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.
|
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | 628fd7a | Aug 08 2026, 01:23 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | 628fd7a | Aug 08 2026, 01:23 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-do | 628fd7a | Aug 08 2026, 01:23 PM |
Scope checkThis PR changes 2,070 lines across 16 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. |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/registry-verification
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
There was a problem hiding this comment.
Pull request overview
Extends the infra/emdash-bot “next-gen” state machine to support maintainer-triggered investigation and a fix loop (preview → reporter confirm/reject → draft PR or branch reap), and makes the previously-promised generated artifact contract (machine.json + BOT_STATE_MACHINE.md) real with a generator and drift tests.
Changes:
- Adds new bot machine states/events/actions for investigation + fix-loop, with corresponding router/orchestrator wiring and GitHub side-effects (draft PR + branch deletion).
- Introduces deterministic artifact renderers + a
bot:generatescript, commits the generated artifacts, and adds unit drift tests to prevent future drift. - Adds unit/integration test coverage for authorization gating and the new fix-loop paths (confirm/reject/expire/skip).
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| infra/emdash-bot/tsconfig.json | Enables TS extension imports needed by the artifact-renderer module graph. |
| infra/emdash-bot/tests/unit/router.test.ts | Adds router-level assertions covering new investigation + fix-loop transitions and command offerings. |
| infra/emdash-bot/tests/unit/machine-artifacts.test.ts | Adds byte-for-byte drift tests for committed machine artifacts. |
| infra/emdash-bot/tests/integration/orchestrator.test.ts | Adds orchestrator integration coverage for maintainer gating, fix loop happy path, rejection/expiry reaping, and skipped fixes. |
| infra/emdash-bot/scripts/machine-artifacts.ts | Adds deterministic renderers for machine.json and BOT_STATE_MACHINE.md. |
| infra/emdash-bot/scripts/generate-machine.ts | Adds a generator script to write the rendered artifacts to disk. |
| infra/emdash-bot/package.json | Adds bot:generate script hook for artifact regeneration. |
| infra/emdash-bot/BOT_STATE_MACHINE.md | Commits generated state machine documentation. |
| infra/emdash-bot/.flue/lib/router.ts | Extends investigation modes and makes outcomeFromResult mode-aware for diagnose/fix. |
| infra/emdash-bot/.flue/lib/orchestrator.ts | Implements fix-loop expiry window + new side effects (open draft PR, reap branch). |
| infra/emdash-bot/.flue/lib/machine.ts | Adds new states/events/actions/transitions for investigation + fix loop. |
| infra/emdash-bot/.flue/lib/machine.json | Commits generated runtime machine snapshot. |
| infra/emdash-bot/.flue/lib/github.ts | Adds deleteBranch() helper and supports draft PR creation. |
| infra/emdash-bot/.flue/lib/comments.ts | Adds user-facing bot comment strings for new states and fix-loop messaging. |
| infra/emdash-bot/.flue/agents/investigate.ts | Adds diagnose/fix modes and adjusts prompting to enforce “diagnose-only” behavior. |
| .oxfmtrc.json | Excludes generated bot artifacts from formatting. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| "build": "vite build", | ||
| "deploy": "vite build && wrangler deploy", | ||
| "cf-typegen": "wrangler types", | ||
| "bot:generate": "node scripts/generate-machine.ts", |
| const botRoot = fileURLToPath(new URL("..", import.meta.url)); | ||
|
|
||
| writeFileSync(`${botRoot}.flue/lib/machine.json`, renderMachineJson()); | ||
| writeFileSync(`${botRoot}BOT_STATE_MACHINE.md`, renderMachineDoc()); |
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
There was a problem hiding this comment.
This is a clean, additive slice of the bot next-gen work. It extends the state machine with maintainer-triggered investigation and a gated fix loop, wires the new actions into the Orchestrator DO and GitHub helpers, and finally makes the generated machine.json + BOT_STATE_MACHINE.md contract real with deterministic renderers and a byte-for-byte drift test. The approach fits the existing architecture: payload-derived actor authorization is preserved, the classifier only selects from state-appropriate non-destructive commands, and the expensive maintainer-only actions are properly gated in EVENTS.
I read the full diff and traced the new states/events through machine.ts, router.ts, orchestrator.ts, comments.ts, the investigate agent, and the GitHub helpers. The transition table is internally consistent (17 states / 30 events / 88 transitions, no duplicates, every non-terminal state has an outgoing edge, every terminal has a reopen edge, all states reachable). Tests cover the new authority rejection, investigation verdicts, fix-loop happy path, reject/expire branch reap, and a regression for the skipped-fix transition.
The only real gap is that the structural validateMachine() validator is exported and described as part of the CI contract, but it is never called by the generator or the drift tests. Because the drift test only checks that the committed artifacts match the rendered output, a malformed machine could still produce matching artifacts and pass CI while violating reachability/terminal-reopen guarantees. Enforcing it is a straightforward follow-up.
|
|
||
| const botRoot = fileURLToPath(new URL("..", import.meta.url)); | ||
|
|
||
| writeFileSync(`${botRoot}.flue/lib/machine.json`, renderMachineJson()); |
There was a problem hiding this comment.
[needs fixing] validateMachine() is exported from machine.ts and the header there says it is "run by the generator and in CI so a malformed edit to this file fails fast", but scripts/generate-machine.ts never calls it. The byte-for-byte drift test only verifies that the committed artifacts match the rendered output; it cannot catch structural problems such as unreachable states, missing reopen edges, or duplicate (state, event) transitions. Wire the validator into the generator so malformed machines fail before the artifacts are written, and add an assertion in tests/unit/machine-artifacts.test.ts.
| writeFileSync(`${botRoot}.flue/lib/machine.json`, renderMachineJson()); | |
| import { renderMachineDoc, renderMachineJson } from "./machine-artifacts.ts"; | |
| import { validateMachine } from "../.flue/lib/machine.ts"; | |
| const botRoot = fileURLToPath(new URL("..", import.meta.url)); | |
| const problems = validateMachine(); | |
| if (problems.length > 0) { | |
| throw new Error(`Invalid machine.ts: ${problems.map((p) => p.message).join("; ")}`); | |
| } | |
| writeFileSync(`${botRoot}.flue/lib/machine.json`, renderMachineJson()); | |
| writeFileSync(`${botRoot}BOT_STATE_MACHINE.md`, renderMachineDoc()); |
Also add to tests/unit/machine-artifacts.test.ts:
import { validateMachine } from "../../.flue/lib/machine.ts";
test("machine.ts passes structural validation", () => {
expect(validateMachine()).toEqual([]);
});
What does this PR do?
Slice 2 of the bot next-gen stack (on #2375). Extends the emdash-bot state machine with maintainer-triggered investigation and the fix loop, and makes the machine's generated-artifact contract real.
investigating→reproduced/not_reproduced/needs_info;fixing→preview_building→awaiting_reporter), 6 new events (investigate+fixcommands, maintainer-actor-only;agent.needs_info;preview.ready/preview.failed;expire), 4 new actions (investigate.diagnose,investigate.fix,openDraftPr,reapBranch). All additive;validateMachineclean at 17 states / 30 events / 88 transitions. Fix-loop shape per the design: reproduced → (maintainerfix) → candidate → preview → reporter confirms → draft PR; reject or 14-day silence reaps the branch.decision.tobranching where reused agent events now land in two targets (legacy paths keep their CTAs); investigation modes in router + agent (diagnose mode forbids fixing);openDraftPr/reapBranchon existing github helpers;expirerides the existing hourly tick with a 14-dayREPORTER_SILENCE_WINDOW_MS.machine.ts's header has always promised generatedmachine.json+BOT_STATE_MACHINE.mdwith drift checking — the generator and artifacts did not exist. This addspnpm bot:generate(deterministic renderers), commits both artifacts, and adds a byte-for-byte drift test to the unit suite.Authorization is payload-derived end-to-end: actor identity comes from the webhook, never model output, so the classifier cannot escalate a reporter into maintainer-only actions (covered by an integration test).
Closes #
Type of change
Checklist
pnpm typecheckpasses — n/a-with-note: same pre-existinginfra/emdash-botred state as chore(emdash-bot): port to Flue 2.0.3 with Workers traces (bot next-gen 1/5) #2375 (untracked in CI; zero new error classes)pnpm lintpasses — oxlint--type-aware --deny-warningsexit 0 on changed scopepnpm testpasses — unit 96/96 (incl. artifact drift check), integration 35/35 (workers pool)pnpm formathas been runAI-generated code disclosure
Screenshots / test output