* 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>
Blur the active element before capturing so a hydration focus race on the
TipTap toolbar cannot leave a button highlighted in some runs and not
others, which produced spurious diffs on the content-editor and content-new
screens.
Also decide visual drift from git status of the snapshots directory rather
than from a non-zero Playwright exit. A flaky render can fail the first-pass
diff yet regenerate byte-identical baselines under --update-snapshots; the
old candidate-emptiness check always saw the committed baselines and flagged
drift regardless. Now a clean tree after regeneration is treated as flake or
infra failure and keeps the check green with a warning.
* ci: skip changeset validation when a PR adds no changeset
The changeset-validate guard checked working-tree presence via compgen,
but pull_request CI runs on the merge commit, whose tree also contains
changesets already accumulated on main. The guard therefore ran
changeset status --since=origin/main, which counts only changesets added
since main, found zero, and failed any PR that legitimately needs no
changeset. Gate on the same git-diff-since-merge-base notion --since uses
internally so the two agree.
* ci: tighten changeset validation comment
* ci: accept visual baselines via /accept-baselines comment
Replace the reaction-poll apply job with an issue_comment trigger. A
maintainer comments /accept-baselines; the job checks write access via
author association, finds the measure run for the PR head, and commits
the candidate Linux baselines. Drops the schedule poller, the all-PR
scan, the reactions API, and the collaborator-permission lookup, and
posts explicit result comments. Update the sticky comment and docs copy.
* ci: address review on /accept-baselines apply
- Require an exact `/accept-baselines` comment (matches format-command)
so quoted or suffixed text can't trigger a privileged write
- Grant issues: write for the reaction API, and make the 👀 ack
best-effort so it can't block the apply
- Select the newest completed measure run and tell the maintainer to
wait when it's still in progress, instead of a misleading "expired"
- Broaden the failure comment to any post-gate failure
* test: add visual regression suite for the admin UI
Adds a Playwright visual-regression suite covering key admin screens in
LTR (English) and RTL (Arabic), gated behind EMDASH_VISUAL=1. Adds the CI
trio: an untrusted measure job, a trusted report job that comments diffs,
and a reaction-gated apply job that commits accepted Linux baselines on a
maintainer 👍. Adds inert data-testid hooks to mask drift-prone timestamps
and version strings.
* fix: address review on visual regression suite
- Widen visual.yml path filter to the whole e2e tree + playwright.config
so fixture/seed/setup changes that alter screenshots trigger a measure run
- Await document.fonts.ready without returning the non-serializable
FontFaceSet, which would throw in Playwright
- Import the canonical ServerInfo type from the fixtures instead of
redeclaring a narrowed copy
- Report workflow: on a bootstrap run with no prior baselines, show the
proposed candidate baselines instead of an empty "0 screens changed" table
- Add an empty changeset (only published-package change is inert testids)
* ci: run visual regression on lockfile/manifest changes
A Playwright or Chromium version bump alters pixel output, so a
pnpm-lock.yaml or package.json change can drift committed baselines
without touching any source path in the existing filter.
actions/checkout v7 refuses to check out fork PR code from a
workflow_run workflow unless the step explicitly sets
allow-unsafe-pr-checkout: true. Since the v7 bump (#1611), every
"Query Counts — Apply" and "Auto Format — Apply" run that reaches the
fork checkout fails with "Refusing to check out fork pull request
code", so fork PRs no longer get snapshot updates or auto-formatting
pushed back.
Both jobs were already designed for this trust boundary: they never
execute code from the fork tree — they only apply inert artifacts
produced by the trusted measure/format runs (JSON copies, git apply)
and push with a scoped app token via GIT_ASKPASS, with
persist-credentials: false. Opt in explicitly and document why the
checkout is safe.
Runs the format pass with the updated formatter and pins oxfmt in
the auto-format and format-command workflows so npx doesn't silently
fetch a newer version than what's used locally.
* ci: harden GitHub Actions and route fork auto-format through workflow_run
Scope every create-github-app-token to the permissions it uses instead of the
app's blanket installation set (auto-extract, sync-templates, release,
dependabot-approve, format-command, query-counts-apply, auto-format-apply).
Move attacker-influenceable values out of run/script blocks into env vars to
close template-injection: dependabot-approve dependency names, and the fork
ref/repo in format-command and query-counts-apply.
Replace auto-format's pull_request_target fork checkout (now refused by
actions/checkout v7) with a two-stage producer/applier. auto-format.yml runs
unprivileged on pull_request and uploads a patch artifact; auto-format-apply.yml
applies it via workflow_run, never executing PR-authored code.
Bump zizmor-action to v0.5.7 and pin the tool to 1.26.1. Document-ignore
use-trusted-publishing (pkg.pr.new preview), adhoc-packages (bot runner), and
archived-uses (CLA action) with justifications.
* fix(ci): grant scopes the token actually needs and make patch binary-safe
Address adversarial review of the token scoping:
- sync-templates: the sync script opens a PR via `gh pr create`, so the
token needs pull-requests:write in addition to contents:write — otherwise
the release-time template sync 403s after pushing the branch.
- format-command: reacting to the triggering comment hits the
issues/comments reactions API, which is issues-namespace; add issues:write
so the (pre-format) reaction step can't 403 and break /format.
- auto-format: diff the patch with --binary so a file git treats as binary
produces an applicable patch instead of an unappliable stub.
The query-counts workflow regenerates all four snapshot files
(.snapshot.{sqlite,d1}.json counts and .queries.{sqlite,d1}.json SQL
text) but its drift detection and the Apply workflow only tracked the
two count files. The query-text files were regenerated in CI and thrown
away, so they only changed when a contributor ran the harness locally
and committed them by hand. The D1 text file in particular needs a
wrangler preview run, which rarely happens, so it silently drifted from
the real emitted SQL.
Track all four files in drift detection and commit all four from the
Apply workflow, so a change that alters a query's text but not its count
keeps the recorded SQL accurate.
* test(repro): Image.astro src resolution for migrated nodes (#1404 render follow-up)
* test(repro): faithful container render of migrated image node (#1404 render follow-up)
Renders the real RAFYC image node through astro-portabletext via the production
dispatch (type.image -> Image.astro), default vs delegating override vs no-locals.
All three emit a valid src: the override receives the full node intact and
delegation to <Image> does NOT render empty. Confirms alignment is dropped.
* feat(image): honor image alignment + surface display size for migrated images (#1404)
Renderer (core/Image.astro): add alignment to the PT image node props and
emit an emdash-image--align-{left,right,center,wide,full} figure class with
default CSS (floats wrap text; wide/full span the column).
Editor (admin): thread alignment through the PortableText<->TipTap serializer
(both directions), the TipTap image node attribute/commands, and add an
alignment selector to ImageDetailPanel. Also relax the Display Size gate from
(width && height) to (src) so migrated WordPress images, which carry only
displayWidth/displayHeight and no original dims, can still be resized; the
aspect-ratio lock and reset-to-original now degrade gracefully when originals
are unknown.
Repro test asserts the renderer now emits the alignment class.
* feat(image): reflect alignment in the editor node view (#1404)
Float left/right (text wraps) and size center/wide/full in the TipTap image
NodeView so the editor preview matches the published <Image> output.
* style: format
* chore: add changeset for image alignment fix (#1404)
* test: genericize #1404 repro comments (drop downstream references)
* feat(image): un-float aligned images on narrow screens (#1404)
Add a max-width:640px media query so left/right-aligned images drop to
full-width blocks on phones instead of staying floated and cramped.
* test(core): stop render tests breaking the plain-node unit run
The #1404 follow-up added tests/repro/*.render.test.ts (which import
.astro components) plus vitest.repro.config.ts (Astro Vite plugin), but
never wired the runner up. The default vitest.config.ts swept the render
test into its plain-node run, which cannot transform .astro, so the whole
core suite failed to load (0 tests collected).
- vitest.config.ts: exclude tests/repro/**/*.render.test.ts (needs Astro)
- package.json: add test:repro script (mirrors test:smoke/test:integration)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: run repro render tests in the Tests job
5c26f319 split the Astro render test out of the plain-node test:unit
run (vitest.repro.config.ts + test:repro script) but the CI invocation
was never committed, so the #1404 alignment/display-size render
coverage would not execute in CI.
Run the repro config right after test:unit in the Tests job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(core): address review — drop fake repro test, de-number titles
Per @ascorbic review on #1406:
- delete tests/repro/image-render.test.ts: it only exercised local
reimplementations (buildRenderMediaUrl), not the real component; the
real coverage lives in image-render.render.test.ts.
- strip the 1./2./3. numbering from the render test titles.
- drop the throwaway #1404-follow-up header line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Matt Kane <mkane@cloudflare.com>
The Query Counts label workflow was path-filtered on the snapshot files, so
it only ran when a PR's diff touched them. That created a catch-22: once a
previously-changed snapshot matched base again (reverted, or base caught up
via a merge), the PR diff no longer touched the files, so the workflow never
ran and its self-heal (remove label, delete comment) could never fire. The
result was a stale "query counts dropped" comment lingering on a PR whose net
diff no longer changes any counts.
Drop the paths filter and let the script decide from the actual base/head
diff on every run: add the label and upsert the comment when snapshots
differ, remove the label and delete the comment when they match. The job
only reads the snapshot files via the API and manages labels/comments, so it
never executes PR code despite pull_request_target.
Skip the Feature Discussion-link requirement for maintainers (OWNER,
MEMBER, COLLABORATOR via author_association) and accept the CONTRIBUTING
checkbox even when the markdown link is stripped or reworded.
Updates the fix implementer, PR review agent, and the /bonk and /review
kimi alias to Kimi K2.7 Code. Issue and reply classifiers stay on
kimi-k2.6 since they do structured classification, not code.
* ci: host plugin tarballs as release assets, drop marketplace seed
Attach each published sandboxed plugin's bundle to the GitHub release
changesets creates, giving the decentralized registry (RFC 0001) a
stable public URL per version. Remove the legacy marketplace seed
workflow, which has been failing since the post-#1057 plugin layout
broke its bundler and is superseded by the registry.
* ci: harden plugin tarball attach script
Address review feedback: guard per-directory package.json parsing so one
bad manifest doesn't abort the step, treat an empty PUBLISHED_PACKAGES
output as no-op, and fail fast when GITHUB_REPOSITORY is missing. Note
the publish-only path limitation in the workflow.
* test(e2e): wire marketplace + noop sandbox into the CF fixture
Mirrors the Node fixture: reads EMDASH_MARKETPLACE_URL and uses the runtime-
agnostic noop sandbox runner (createNoopSandboxRunner, no cloudflare:workers
import) so the marketplace browse/detail specs run against the workerd target.
Avoids the CF sandbox()/LOADER-binding setup, which is only needed at plugin
install time, not for the browse UI.
* test(e2e): run the full suite against Cloudflare, sharded
Expands the CF lane from the content-types smoke to the whole e2e suite (4 shards;
fewer than the Node lane's 8 because each shard boots its own workerd dev server).
235/237 specs pass on the workerd runtime.
Skips one CF-specific failure: 'invited user appears in the users list' — after a
passkey-invite registration the user isn't read back on workerd (passes on Node,
reproducible in isolation). Suspected D1 Sessions read-after-write under miniflare;
flagged for maintainers, skipped so the lane stays green.
* test(e2e): skip i18n translation-sibling check on Cloudflare
CI surfaced a second CF-specific failure with the same write-then-read signature
as the invite-flow skip: on the freshly created FR translation page the EN sibling
isn't read back in time, so its 'Edit' link doesn't render — though the translation
is created and navigable (the other i18n specs pass). Suspected D1 Sessions
read-after-write under miniflare dev; flagged for maintainers, skipped on CF so the
lane stays green.
* ci: shard the Cloudflare e2e lane 8 ways like the Node lane
Wall-clock breakdown showed per-shard setup is only ~30s (dev-server boot + seed
+ warm); the ~8min was test execution, not boot overhead. So 4 shards left it at
~9min vs the Node lane's ~5min. Tests parallelize near-linearly — 8 shards brings
it in line.
* test(e2e): add minimal Cloudflare adapter fixture
Mirrors e2e/fixture but swaps the Node adapter + SQLite for the Cloudflare
adapter + D1/R2, so astro dev runs the workerd SSR module runner. Foundation
for a Cloudflare e2e lane that catches workerd-only regressions (#1292 class).
* test(e2e): parametrize harness for the Cloudflare target
EMDASH_E2E_TARGET=cloudflare boots e2e/fixture-cloudflare under astro dev
(workerd) instead of the Node fixture. Gate setup on dev-bypass actually
returning 200 rather than the server merely responding -- the Cloudflare dev
runner's Vite dep optimizer 500s on cold start until it finishes pre-bundling.
Clear miniflare D1/R2 state per run for isolation. Existing specs run
unmodified against both targets.
* test(e2e): address review — track CF seed, fix teardown/markers, seed content server-side
Review findings on #1320:
- Commit the CF fixture's .emdash/seed.json (it was gitignored, so the fixture
silently fell back to the default seed in a fresh checkout). Un-ignore the dir.
- global-teardown cleaned only the Node fixture path; use info.workDir so the CF
fixture's .astro/.wrangler/.emdash artifacts are cleaned too.
- ensureFixtureDepsBuilt gated on a single marker; check all required artifacts
per target (cloudflare + plugin-color), else a half-built tree boots a broken
fixture.
Root-cause fix for cold-start seeding: content seeding hand-rolled per-item API
POSTs, each the first hit to a route the workerd dev runner hadn't compiled —
racing the Vite optimizer and 500ing on a cold cache. Move the plain posts/pages
into the seed file's content block (applied server-side during dev-bypass) and
resolve their ids by slug (the list GET warms the content route before the one
remaining dynamic flow, the media upload + image post). Drop the CF fixture's
astro build script (it polluted the dev run) and give the slower workerd target
a 90s test timeout.
CF: content-types 11/11 warm and green cold; node: content-types 11/11, search
20/20.
* ci: add Cloudflare/workerd e2e smoke job
Runs the content-types spec against EMDASH_E2E_TARGET=cloudflare so the foundation
is actually exercised on every PR, not dormant. Single job (no sharding) scoped to
one spec that drives the admin SPA, schema CRUD, and seeded content end-to-end on
the workerd runtime. Broad sharded coverage + paths-gating is a follow-up.
* test(e2e): warm the admin's data routes before specs run
CI's slower 2-core runner exposed a cold-start gap: the content-types page's
client-side fetch to /_emdash/api/schema/collections hit that route cold, the
workerd optimizer served a 500, and the admin rendered an empty table — failing
the first specs (even on retry, since the route was still compiling). idsBySlug
warmed the content routes but not the schema/media routes. Warm those (authed)
to a 200 before tests start, so the browser's first fetches don't race the
optimizer.
* feat(triage): freeform maintainer directives on reproduced issues
Adds maintainer-reply.yml: when an issue is in triage/reproduced or
triage/by-design, an authorized maintainer can comment `@emdashbot
<directive>` to direct an implementation. A small Flue classifier maps
the freeform intent (implement/close/takeover/unclear); an implement
directive fires a maintainer-directive repository_dispatch at
investigate.yml, which runs a directed investigation (overriding the
judgment and fix gates) and routes the produced fix through the existing
awaiting-reporter loop, where confirm/reject already lives.
Closes the gap where the bot reproduced a bug but deferred the fix (e.g.
needs-design-decision with options) and there was no way to reply.
* fix(triage): don't let proceed/steer ambiguity fall back to unclear
proceed and steer resolve to the same implement action, so the classifier
shouldn't pick unclear when the only dilemma is between them. Reword the
prompt: unclear is for no actionable instruction at all; the cost caution
applies to close/takeover, not to choosing a fix.
* refactor(triage): collapse proceed/steer into a single implement intent
They resolved to the same action, so the split only gave the classifier a
cosmetic decision to make (and a section of prompt apologising for it). The
directive field carries the actual which-option/what-change information.
* fix(triage): don't tell the maintainer the close is left 'to a maintainer'
The commenter is the authorized maintainer, so deferring the close 'to a
maintainer' is circular. Address them directly: the bot never auto-closes,
close it when ready.
* fix(triage): read classifier result from a file, not flue stdout
The reporter-reply classifier scraped its result out of `flue run`'s stdout,
but flue interleaves build-log lines and pretty-prints the returned value --
both defeat the line-by-line and slurp parses, so every classification
silently defaulted to `unclear`. Reporters confirming "yes, fixed" got
re-asked forever and no PR ever opened from the AI path (only the
deterministic @emdashbot confirm path worked). Seen live on #1242, #1250.
investigate.ts already solved this by writing its result to a file
(INVESTIGATE_RESULT_PATH) and reading that; the classifiers never did.
Add a shared persistClassifierResult() helper that writes to
CLASSIFY_RESULT_PATH, and read that file in both reporter-reply and
maintainer-reply instead of scraping stdout. The new classify-maintainer-reply
copied the same fragile scrape, so this fixes it before it ever shipped.
* fix(triage): address PR review findings
- botContext: empty string (no bot comments) now falls back to the prompt
cue via a truthiness check; `??` only caught null/undefined.
- directed run with an abandoned fix + intended-behavior verdict no longer
misroutes to the by-design branch; the parse step gates intended-behavior
on not-directed so it falls through to the directed-aware reproduced wording.
- whitespace-only directive (possible via manual workflow_dispatch) normalizes
to empty so `directed` and the payload reflect only a real instruction.
- close handler block-quotes every line of multi-line reasoning, not just the
first.
- correct the classify-maintainer-reply header: permission-API admin/write/
triage on reproduced/by-design, not OWNER/MEMBER/COLLABORATOR on any triage/*.
* fix(triage): neutral botContext fallback for by-design issues
The fallback fires for triage/by-design too, where the bot concluded intended
behavior rather than reproducing a bug. Drop the reproduced/proposed-fix
assumption for neutral wording.
* refactor(triage): read the classifier result via CLASSIFY_RESULT_PATH
The classify steps set CLASSIFY_RESULT_PATH but then hard-coded the literal
path in every read. Derive RESULT_PATH from the env var (failing loud if
unset) so the path has one source of truth.
The reporter-reply negative/retry path re-triggered investigate.yml with
`gh workflow run` (workflow_dispatch), which failed in production because
the emdashbot App token has contents/issues/pull-requests write but not
actions:write (required for workflow_dispatch). The job caught the failure
and fell back to asking a maintainer to re-trigger manually, so retries
never actually ran.
Switch to repository_dispatch (event type `reporter-retry`), which needs
only contents:write that the App already has. reporter-reply builds the
payload with jq (so the attacker-controlled retry text is JSON-escaped,
never interpolated) and POSTs to /repos/{repo}/dispatches. investigate.yml
gains a repository_dispatch trigger and reads issueNumber/retryContext from
client_payload, falling back through the existing workflow_dispatch inputs.
Hardening from review: validate the dispatched issue number (positive int,
no leading zero) BEFORE it is interpolated into the issues API path, and
reject PR numbers on the dispatch path (the issues API returns PRs too;
only the labeled path was PR-guarded). Both files must land together since
repository_dispatch runs the default-branch workflow definition.
The current gh CLI on ubuntu-latest rejects `--slurp` combined with
`--jq` ("the --slurp option is not supported with --jq or --template"),
failing the live-check and retry-count queries. Keep `--paginate --slurp`
(needed to flatten pages into one array) and run the jq filter as a
separate piped command instead. No behavior change to the filters.
The triage project sync only added/updated cards on label changes; it
never removed them when an issue closed, so the Auto-Triage board
accumulated closed issues indefinitely.
Add `closed` and `reopened` to the issues trigger. The sync now keys the
add-vs-remove decision on the issue's open/closed state: closing archives
the card (reversible, leaves active board views, kept in the project
archive); reopening unarchives and re-syncs it from labels. Because
addProjectV2ItemById returns an existing item as-is, the upsert path
unarchives explicitly so a reopened card returns to the active board.
Handling state in the main loop (not only on the `closed` event) means
label edits on already-closed issues and the manual backfill reconcile
too, so the board self-heals to "open triaged issues only".
Card lookup matches on the project node id, not the per-owner project
number, so a foreign project that also happens to be #3 can't be
targeted. The backfill loop is per-issue try/catch so one failure does
not abort the batch; single-issue runs still fail loudly.
Previously the reporter-reply workflow only acted on comments from the
original issue author. Maintainers can now confirm or reject a candidate
fix on the reporter's behalf (e.g. when the reporter has gone quiet or
the maintainer can verify the result themselves).
Authorization is done in the live-check step via the collaborators
permission API (write or triage role), not the event payload's
author_association, which reports NONE for maintainers with private org
membership. The job if: is correspondingly coarsened (not-a-PR, not-a-bot,
label present) so those maintainers are not filtered out before the check.
To keep drive-by maintainer chatter from driving state, a maintainer must
opt in with an explicit directive at the start of a line: @emdashbot
confirm or @emdashbot reject. These map deterministically to
positive/negative and skip the AI classifier. The reporter path is
unchanged (any reply is classified). investigate.yml's verification ask
now documents the directives.
* ci: validate changesets reference real packages
Adds a Changeset Validation job that runs `pnpm changeset status` on every PR. It assembles the release plan and fails fast if a changeset names a package that isn't in the workspace, the failure that broke the Release workflow in #1267.
* ci: diff changeset status against origin/main with full history
The status CLI computes changed packages since the base branch before assembling the plan, so it needs git history and the remote-tracking ref. Shallow checkout failed with 'Failed to find where HEAD diverged from main'. Use fetch-depth: 0 and --since=origin/main, which resolves for branch and fork PRs alike.
Restrict the dev/production groups to minor and patch bumps so major updates break out into individual PRs. A grouped major (@vitejs/plugin-react 4 -> 6) broke the whole batch in #1262; isolating majors keeps the safe minor/patch updates flowing and makes breaking bumps reviewable on their own.
Add the npm ecosystem (covers pnpm + workspace catalogs) and a cooldown that exceeds pnpm's minimumReleaseAge install gate so Dependabot never proposes a version pnpm will refuse to install in CI.
The investigate workflow stripped the bot/ prefix from the fix branch
name when building the pkg.pr.new install command, producing
emdash@fix-N. pkg.pr.new resolves branches by their full ref, so that
URL 404s while emdash@bot/fix-N resolves. Use the full branch name.
* ci: publish all public packages as previews and pin pkg-pr-new
Derive the pkg.pr.new publish set from globs (./packages/*,
./packages/plugins/*) instead of a hardcoded list. pkg.pr.new skips
private packages, so the test fixtures are excluded automatically while
every public package, including the previously missing registry-client,
plugin-types and auth-atproto, now gets a preview.
This keeps preview installs self-consistent: emdash's preview references
sibling packages via workspace:*, and pkg.pr.new can only rewrite those
to matching preview URLs when the siblings are published in the same
run. Omitting one made the dep fall back to npm's released version,
which broke when source had drifted (e.g. registry-client's ./env
export added without a release).
Also pin pkg-pr-new as a root devDependency and run it via pnpm exec
rather than pnpm dlx, per the tool's CI guidance.
* fix(ci): correct investigate-bot identity and split triage labels
Two orchestrator-workflow fixes:
- Commit identity was emdash-bot[bot] / bot@emdashcms.com, but the
GitHub App slug is emdashbot. Use emdashbot[bot] and the
users.noreply.github.com attribution email, matching every other
workflow. This also makes the github.actor guards in auto-format and
auto-extract match the bot's own commits.
- intended-behavior outcomes now get a new triage/by-design label
instead of sharing triage/reproduced with confirmed bugs. The two
need opposite follow-up (likely close vs. needs a fix), so they
should not share a label.
* feat(flue): broaden investigate-bot fix gate, run fix on a cheaper model
The fix stage only ran at verify=bug AND diagnose.confidence=high, where
high meant 'mechanical, one-line, no ambiguity'. That conflated two
independent questions: is the root cause certain, and is the fix
obvious? Real, fixable bugs (e.g. #1178, #1199) were parked at
triage/reproduced because one clearly-correct fix existed among several
shapes, which forced a medium rating.
Decouple the axes:
- confidence now rates root-cause certainty only.
- a new fixApproach (mechanical | clear-best-option | needs-design-decision)
rates fix clarity.
- the gate becomes verdict=bug AND confidence!=low AND
fixApproach!=needs-design-decision.
Diagnose also now emits a concrete proposedFix (always), which feeds the
fix stage as its spec and doubles as the maintainer's starting point
when the fix is deferred.
The fix stage runs on a separate, cheaper agent (kimi-k2.6) in its own
session: the reasoning is already done, so it is guided implementation.
It shares the on-disk checkout, so staged edits still reach the
orchestrator. Configurable via FLUE_FIX_MODEL.
Reframe the skill cost model: the output is a reporter-verified
candidate branch a maintainer reviews, not a merge, so a clear,
test-backed fix is worth attempting even when it is more than a
one-liner. Update diagnose/verify/fix skills, _INVESTIGATE.md and
README; delete the stale PLAN.md.
* fix(ci): wire triage/by-design into label cleanup and project sync
Addresses review on #1259. The new triage/by-design label was missing
from two places:
- investigate.yml's reproducing-transition cleanup loop, so a
re-triggered by-design issue could carry triage/by-design alongside
triage/reproducing.
- triage-project-sync.yml's STATE_BY_LABEL/PRECEDENCE, so by-design
issues resolved to no state and stopped syncing to the board.
Maps triage/by-design -> 'By design' board option (terminal verdict,
ranked just below reproduced in precedence). The 'By design' single-
select option must be added to Project #3's 'Triage State' field; until
then the sync warns-and-skips rather than failing.
* chore(triage): rename triage state labels to triage/*, add project sync
Applies the slash=state / colon=trigger convention to the triage bot. The
bot:repro trigger stays; the state labels become triage/*:
bot:reproducing -> triage/reproducing
bot:reproduced -> triage/reproduced
bot:awaiting-reporter -> triage/awaiting-reporter
bot:verified -> triage/verified
bot:not-reproduced -> triage/not-reproduced
bot:failed -> triage/failed
bot:skipped -> triage/skipped
- Rename across investigate.yml, reporter-reply.yml, the Flue classifier
comment, and the .flue docs.
- Drop the per-run "ensure labels exist" step; labels are managed once and
applying them is enough.
- Add triage-project-sync.yml: on issue label changes it recomputes the
issue's canonical triage state from its current labels and sets the
Auto-Triage board's "Triage State" field, authed by the emdashbot app token.
Out of band: the emdashbot App needs org "Projects: Read and write", and the
live labels are renamed post-merge (gh label edit preserves associations).
* style: format
* chore(triage): scope the project-sync app token to least privilege
Address zizmor: request only organization-projects:write + issues:read, and
limit the token to this repo, instead of inheriting the app's full
installation permissions across all repos.
---------
Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
Playwright's browser extraction deadlocks deterministically on the
runner (frozen mid-write on libwidevinecdm.so; runs 26630115530,
26632563684, 26633933945), and a Playwright test the bot writes but
cannot run is unverified guesswork anyway. Remove Playwright entirely:
- investigate.yml: delete the debug cache-dump step and the Playwright
browser install step. agent-browser (installed earlier) is the only
browser stack now.
- repro-admin / repro-public skills: reproduce and confirm via
agent-browser; the durable artifact is screenshots plus a replayable
transcript. Do not write Playwright tests. Added an explicit note that
extension/profile-only bugs (e.g. #1200, a password-manager focus
steal) are a real class the clean headless browser cannot trigger.
- fix skill: write a vitest regression test when the bug has a unit/
integration surface; for browser-only bugs, verify via agent-browser
and hand the durable test to the maintainer.
- investigate.ts: drop the now-unused `playwright-test` approach value.
ci.yml keeps Playwright for the real e2e suite -- it caches the browser
and only installs on a cache miss, so it does not hit this path.
`playwright install` deterministically hangs right after the browser zip
hits 100% (runs 26630115530, 26632563684); manual re-runs stall at the
same point, so it is not a flaky CDN node. agent-browser is built on
playwright-core and shares ~/.cache/ms-playwright, so registry/lock
contention in that shared cache is the leading hypothesis.
Add temporary instrumentation to capture the cause without more
round-trips: dump the cache + lock state agent-browser leaves behind,
then run `playwright install` in the background with DEBUG=pw:install and
snapshot the install log, download processes, cache tree, lock/incomplete
markers, open files, and sockets every 15s so the hang is captured rather
than opaque. Revert to the plain install once root-caused.
The agent writes a Playwright test as its durable repro artifact and runs
it with `pnpm exec playwright test`, which needs Playwright's own browser
(separate from agent-browser's Chrome). It wasn't pre-installed, so the
agent ran `playwright install chromium` mid-run; in run 26630115530 that
sat for 15+ minutes behind a buffered pipe with no progress until the run
was cancelled.
Pre-install it in a dedicated step using the repo's own e2e CI command
(`playwright install --with-deps chromium`), bounded at 3 minutes since
the runners share Azure's network with the Playwright CDN and a healthy
install is well under a minute. Also tell the repro skills chromium is
already present so the agent runs the test directly.