Skip to content

feat(triage): freeform maintainer directives on reproduced issues#1306

Merged
ascorbic merged 8 commits into
mainfrom
feat/maintainer-reply
Jun 3, 2026
Merged

feat(triage): freeform maintainer directives on reproduced issues#1306
ascorbic merged 8 commits into
mainfrom
feat/maintainer-reply

Conversation

@ascorbic
Copy link
Copy Markdown
Collaborator

@ascorbic ascorbic commented Jun 2, 2026

What

Adds a freeform maintainer-directive path for the triage/reproduced and triage/by-design states — the gap where the investigation bot has reproduced a bug but deferred the fix (e.g. diagnose returned needs-design-decision with options, as in #1281) and there was previously no way for a maintainer to respond to the bot's comment.

How it works

A maintainer comments @emdashbot go with option A+B (or any freeform instruction). Then:

triage/reproduced
  └─ @emdashbot implement A+B   (maintainer-reply.yml)
       ├─ authorize (permission API: admin/write/triage)
       ├─ classify intent (Flue/kimi → implement | close | takeover | unclear)
       └─ implement → repository_dispatch(maintainer-directive, {issueNumber, directive})
            └─ investigate.yml (directed run: overrides the fix gate)
                 └─ bot/fix-<n>  →  triage/awaiting-reporter   (normal ask comment)
                      ├─ reporter: "yes, fixed"        → PR → verified
                      └─ maintainer: @emdashbot confirm → PR → verified

The produced fix routes into the existing awaiting-reporter loop, reusing reporter-reply's confirm/reject verbatim — no new PR-opening code.

This complements reporter-reply.yml (which owns awaiting-reporter); the two gate on disjoint label states and never both fire on one comment.

Files

  • maintainer-reply.yml (new) — issue_comment trigger; gates on reproduced/by-design + non-bot + an @emdashbot wake word at line start; permission-API auth (not the spoof-prone author_association); freeform classify → implement (dispatch) / close (relabel by-design, no auto-close) / takeover (disengage) / unclear (clarify).
  • classify-maintainer-reply.ts (new) — cheap kimi classifier → {proceed|steer|close|takeover|unclear} + extracted directive.
  • classifier.ts — new maintainerIntentSchema.
  • investigate.tsmaintainerDirective payload; a directive overrides the bot's judgment gates (is-it-a-bug at stage 0, is-it-intended at verify) and the fix gate, but not the capability gates (can't reproduce → can't verify a fix → bails honestly).
  • investigate.ymldirective input + maintainer-directive dispatch type, threaded into the agent payload via --rawfile; directed-aware wording on the skipped / not-reproduced / reproduced-no-fix comments.

Security

Mirrors the existing reporter-reply/investigate discipline: attacker-shaped text (directive, classifier reasoning, bot-context, issue title) goes to /tmp via --rawfile, never inlined into shell or an unescaped $GITHUB_OUTPUT heredoc; intent is enum-whitelisted before it reaches a step output; auth is an authoritative permission-API lookup; the directive travels JSON-escaped through repository_dispatch (the App token has contents:write but not actions:write); branch names derive from the validated integer issue number. Auth matches reporter-reply (admin/write/triage).

Review

Ran an adversarial review pass over the diff before opening. Cleared injection, double-fire, the --slurp | jq CI footgun (#1291), and the dispatch wiring. Fixed one finding — a directive was being silently dropped at the stage-0/verify judgment gates with a misleading "declined to reproduce" comment on the by-design path; it now proceeds to a fix. Capability-gate bails got directed-aware wording.

Testing notes

The triage bot runs only in CI (GitHub Actions + Flue agent against the live repo), so this can't be exercised locally. Validated: Flue typecheck + build (3 workflows discovered), all three workflow YAMLs parse, oxfmt clean. No changeset — neither .flue nor .github is a published package.


Try this PR

Open a fresh playground →

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

Tracks feat/maintainer-reply. Updated automatically when the playground redeploys.


Also fixes: AI classifier silently stuck (live bug on #1242, #1250)

While testing, found the existing reporter-reply AI classifier was broken in production — reporters confirming "yes, fixed" got re-asked forever and no PR ever opened from the classifier path (only the deterministic @emdashbot confirm path worked, which is how recent issues actually reached triage/verified).

Root cause: the classifier scraped its result out of flue run's stdout, but flue interleaves build-log lines and pretty-prints the JSON result — defeating both the line-by-line and slurp parses, so every classification silently defaulted to unclear. investigate.ts had already hit this and moved to a file handoff (INVESTIGATE_RESULT_PATH) with a comment calling stdout-scraping fragile; the classifiers never got that treatment — and the new classify-maintainer-reply had copied the same fragile scrape, so this feature would have been broken on arrival.

Fix: a shared persistClassifierResult() helper writes the result to CLASSIFY_RESULT_PATH; both reporter-reply and maintainer-reply now read that file instead of scraping stdout. Mirrors the proven investigate handoff exactly.

After merge, the stuck issues (#1242, #1250) can be unstuck by the reporter re-commenting or a maintainer @emdashbot confirm.

Note: the classifier path can't be exercised locally (needs the CF AI Gateway + Actions); verification is by mirroring investigate's working pattern, and the next reporter reply on any awaiting-reporter issue will confirm it end-to-end.

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.
Copilot AI review requested due to automatic review settings June 2, 2026 21:23
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 2, 2026

⚠️ No Changeset found

Latest commit: cae0c6f

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

PR template validation failed

Please fix the following issues by editing your PR description:

  • This PR does not use the required PR template. Please edit the description to use the PR template. Copy it into your PR description and fill out all sections.

See CONTRIBUTING.md for the full contribution policy.

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet area/ci size/XL labels Jun 2, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

Scope check

This PR changes 640 lines across 6 files. Large PRs are harder to review and more likely to be closed without review.

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

See CONTRIBUTING.md for contribution guidelines.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Jun 2, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

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

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs cae0c6f Jun 03 2026, 06:31 AM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Jun 2, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

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

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground cae0c6f Jun 03 2026, 06:31 AM

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 2, 2026

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

npm i https://pkg.pr.new/emdash@1306

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: cae0c6f

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a maintainer-driven “freeform directive” path to the triage bot so maintainers can respond to reproduced / by-design investigations with authoritative guidance (e.g., choose an option), triggering a directed investigate run that can bypass judgment gates while still respecting capability gates.

Changes:

  • Introduces a new maintainer-reply workflow that authorizes maintainers via the GitHub permission API, classifies intent, and dispatches a directed investigate run (or relabels/disengages/asks to clarify).
  • Threads a maintainer directive through investigate.yml into the Flue investigate payload as maintainerDirective, and updates outcome comment wording for directed runs.
  • Adds a new Flue classifier workflow + schema for maintainer intent classification, and documents the new workflow in .flue/README.md.

Reviewed changes

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

Show a summary per file
File Description
.github/workflows/maintainer-reply.yml New workflow to authorize + classify maintainer @emdashbot directives and act (dispatch/close/takeover/unclear).
.github/workflows/investigate.yml Accepts directive input/dispatch payload; passes it into agent payload and adjusts directed-run messaging.
.flue/workflows/investigate.ts Adds maintainerDirective to context; bypasses judgment/fix gates when directed while keeping capability gates.
.flue/workflows/classify-maintainer-reply.ts New lightweight classifier to map maintainer replies to a fixed intent + extracted directive.
.flue/README.md Documents the new workflow and classifier in the bot architecture overview.
.flue/lib/classifier.ts Adds maintainerIntentSchema and associated type for structured maintainer intent output.

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

Comment thread .github/workflows/investigate.yml Outdated
Comment on lines +146 to +149
# The directive is attacker-shaped multiline text like the body and
# retry context; same treatment -- write to /tmp, never to a step
# output. Empty unless a maintainer-directive dispatch set it.
printf '%s' "$DIRECTIVE" > /tmp/ctx-directive.txt
Comment on lines +3 to +7
// Triggered by .github/workflows/maintainer-reply.yml when a maintainer
// (OWNER/MEMBER/COLLABORATOR) addresses `@emdashbot` on an issue carrying
// a `triage/*` label. The workflow YAML reads the intent from this run's
// output and decides whether to dispatch a directed investigate run, flag the
// issue as by-design, disengage, or ask for clarification.
Comment thread .github/workflows/maintainer-reply.yml Outdated
Comment on lines +370 to +372
if [[ -n "$REASONING" ]]; then
echo
echo "> ${REASONING}"
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Jun 2, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

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

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache cae0c6f Jun 03 2026, 06:31 AM

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.
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/needs-review No maintainer or bot review yet labels Jun 2, 2026
Copy link
Copy Markdown
Contributor

@emdashbot emdashbot Bot left a comment

Choose a reason for hiding this comment

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

This is a well-scoped, architecturally sound feature. The gap it fills is real — when the bot reproduces an issue but defers because of needs-design-decision, maintainers previously had no way to direct the next step. Routing the produced fix through the existing awaiting-reporter / reporter-reply.yml loop is the right reuse choice, and gating the two comment-triggered workflows on disjoint label states prevents double-fire.

I traced the security surface, the shell-safety discipline, and the investigate.ts gate logic against the PR description. The new workflow mirrors reporter-reply.yml's defensive patterns correctly: permission-API auth rather than spoofable author_association, --rawfile for attacker-shaped input, intent whitelisting before step outputs, and repository_dispatch scoped to contents:write. The directed override in investigate.ts correctly bypasses only the judgment gates (bug classification, intended-behavior verdict, needs-design-decision fix gate) while preserving the capability gates (reproduction, fix verification), exactly as claimed.

The implementation is clean, but I found one logic bug in the new classifier: an empty bot-context file passes "" through to payload.botContext, and ?? does not fall back, so the model can be fed an empty context section instead of the fallback instructions.

No changeset is required; neither .flue nor .github is a published package.

Comment on lines +46 to +47
payload.botContext ??
"(unavailable; assume the bot reproduced the issue and either proposed options or pushed a candidate fix)",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[needs fixing] The orchestrator writes "" to /tmp/bot-context.txt when there are no bot comments (e.g. a manually labelled issue or deleted comments), and --rawfile passes that empty string through as payload.botContext. The nullish-coalescing operator ?? only falls through on null/undefined, so the classifier prompt ends up with an empty ## Bot's investigation section instead of the intended fallback text. This degrades classification quality because the model loses the explicit cue to assume the bot reproduced the issue and proposed options.

Fix by using a truthiness check so empty/whitespace-only contexts still get the fallback:

Suggested change
payload.botContext ??
"(unavailable; assume the bot reproduced the issue and either proposed options or pushed a candidate fix)",
payload.botContext?.trim() ||
"(unavailable; assume the bot reproduced the issue and either proposed options or pushed a candidate fix)",

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-rereview Author pushed changes since the last review labels Jun 2, 2026
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.
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jun 2, 2026
ascorbic added 2 commits June 2, 2026 22:52
…iner'

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.
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.
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jun 2, 2026
@ascorbic ascorbic requested a review from Copilot June 2, 2026 22:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment on lines 357 to +360
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
ISSUE_NUMBER: ${{ steps.ctx.outputs.number }}
DIRECTED: ${{ steps.ctx.outputs.directed }}
Comment on lines +3 to +7
// Triggered by .github/workflows/maintainer-reply.yml when a maintainer
// (OWNER/MEMBER/COLLABORATOR) addresses `@emdashbot` on an issue carrying
// a `triage/*` label. The workflow YAML reads the intent from this run's
// output and decides whether to dispatch a directed investigate run, flag the
// issue as by-design, disengage, or ask for clarification.
Copy link
Copy Markdown
Contributor

@emdashbot emdashbot Bot left a comment

Choose a reason for hiding this comment

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

This is a re-review of PR #1306.

Approach

The feature is architecturally sound. It fills a real gap (maintainers directing fixes on deferred reproduced/by-design issues), routes produced fixes through the existing awaiting-reporter / reporter-reply.yml loop rather than duplicating PR-opening code, gates maintainer-reply.yml and reporter-reply.yml on disjoint label states to prevent double-fire, and reuses the proven file-handoff pattern from investigate.ts for classifier results. The security discipline (permission-API auth rather than spoofable author_association, --rawfile for attacker-shaped text, intent whitelisting before step outputs) mirrors the existing workflows correctly.

Prior finding status

My previous finding about the empty botContext fallback is still present and unfixed in the current code.

New findings

I found one additional logic bug during this pass: a directed investigate run whose fix agent abandons can be misrouted to the intended-behavior outcome branch in the orchestrator, incorrectly labeling the issue by-design even though a maintainer explicitly overrode that gate.

What’s clean

  • The shared persistClassifierResult helper and the adoption of CLASSIFY_RESULT_PATH in both reply workflows correctly fix the stdout-scraping bug that was stranding reporter replies in production.
  • Auth, shell-safety, dispatch wiring, and concurrency controls are all correct.
  • No changeset is required (.flue / .github are not published packages).
  • No AGENTS.md product-code conventions are violated.

Verdict

comment — two logic bugs need fixing, but the approach is right and the architecture is sound.

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-rereview Author pushed changes since the last review labels Jun 2, 2026
- 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/*.
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jun 3, 2026
@ascorbic
Copy link
Copy Markdown
Collaborator Author

ascorbic commented Jun 3, 2026

Thanks — all findings addressed in b4bcbaf.

Logic bugs (both reviewers):

  • Empty botContext fallback — the orchestrator passes "" (not undefined) when an issue has no bot comments, so ?? didn't fall through. Switched to payload.botContext?.trim() || "(unavailable…)" so the classifier keeps its prompt cue.
  • Directed run misrouted to by-design — a directed run whose fix is abandoned could carry verdict: intended-behavior and hit the intended-behavior outcome branch, flipping to triage/by-design against the maintainer's explicit override. The parse step now gates that branch on DIRECTED != true, so a directed run falls through to the reproduced branch and its directed-aware "couldn't produce a verified fix" wording.

Copilot's correctness/doc finds:

  • Whitespace-only directive — a blank directive (possible via manual workflow_dispatch) now normalizes to empty, so directed and the payload reflect only a real instruction.
  • Multi-line reasoning in close — block-quotes every line via sed 's/^/> /' instead of prefixing only the first.
  • classify-maintainer-reply header — corrected to "permission-API admin/write/triage on reproduced/by-design", not "OWNER/MEMBER/COLLABORATOR on any triage/*".

@ascorbic ascorbic requested a review from Copilot June 3, 2026 06:04
@ascorbic ascorbic added bot:review Trigger an emdashbot code review on this PR and removed bot:review Trigger an emdashbot code review on this PR labels Jun 3, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Comment thread .github/workflows/reporter-reply.yml Outdated
Comment on lines +281 to +283
set -o pipefail
PAYLOAD="$(cat /tmp/classify-payload.json)"
rm -f /tmp/classify-result.json
Comment on lines +296 to +300
# A clean run writes a single JSON object to the result file. A
# non-zero exit, a missing file, or a non-object means the run did
# not finish -- default to unclear (which re-asks, never acts).
if [[ $EXIT -ne 0 ]] || [[ ! -s /tmp/classify-result.json ]] || ! jq -e 'type == "object"' /tmp/classify-result.json >/dev/null 2>&1; then
echo "::warning::classifier exit=${EXIT} or no result file; defaulting to unclear"
Comment thread .github/workflows/maintainer-reply.yml Outdated
Comment on lines +208 to +210
set -o pipefail
PAYLOAD="$(cat /tmp/classify-payload.json)"
rm -f /tmp/classify-result.json
Comment on lines +223 to +238
# A clean run writes a single JSON object to the result file. A
# non-zero exit, a missing file, or a non-object means the run did
# not finish -- default to unclear (which re-asks, never acts).
if [[ $EXIT -ne 0 ]] || [[ ! -s /tmp/classify-result.json ]] || ! jq -e 'type == "object"' /tmp/classify-result.json >/dev/null 2>&1; then
echo "::warning::classifier exit=${EXIT} or no result file; defaulting to unclear"
tail -n 50 /tmp/classify-stderr.log || true
echo "intent=unclear" >> "$GITHUB_OUTPUT"
exit 0
fi
# Whitelist the intent -- the handler gate must be a known enum or we
# treat it as unclear. Defends against an unexpected model value.
INTENT_RAW="$(jq -r '.intent // "unclear"' /tmp/classify-result.json | tr -d '\r\n')"
case "$INTENT_RAW" in
implement|close|takeover|unclear) INTENT="$INTENT_RAW" ;;
*) INTENT="unclear" ;;
esac
Comment thread .github/workflows/maintainer-reply.yml Outdated
Comment on lines +245 to +246
jq -r '.directive // ""' /tmp/classify-result.json > /tmp/directive.txt
jq -r '.reasoning // ""' /tmp/classify-result.json > /tmp/classify-reasoning.txt
Copy link
Copy Markdown
Contributor

@emdashbot emdashbot Bot left a comment

Choose a reason for hiding this comment

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

Approach

This PR is architecturally sound. It fills a real gap — maintainers previously had no way to direct the investigation bot on deferred triage/reproduced or triage/by-design issues — and it routes produced fixes through the existing awaiting-reporter / reporter-reply.yml loop rather than duplicating PR-opening code. The two workflows gate on disjoint label states (reproduced/by-design vs awaiting-reporter), so they never double-fire. The security discipline mirrors the existing workflows correctly: permission-API auth rather than spoofable author_association, --rawfile for attacker-shaped text, and intent whitelening before step outputs.

Prior findings

  1. Directed run misrouted to intended-behavior on fix abandonmentFIXED. The new DIRECTED boolean derived from directive presence is now checked in both investigate.ts (skipping the early intended-behavior return when directed) and in investigate.yml parse logic ([[ "$VERDICT" == "intended-behavior" && "$DIRECTED" != "true" ]]). A directed run whose fix agent abandons now correctly falls through to the reproduced outcome branch with directed-aware wording.

  2. Empty botContext fallback — still present but is an edge-case suggestion rather than a blocking bug. It only fires when there are no bot comments on an issue that is already in triage/reproduced or triage/by-design (an anomalous state, since those labels are bot-set). The fallback text assumes the bot reproduced the issue, which is misleading for triage/by-design where the bot concluded intended behavior. I recommend neutral wording instead.

What’s clean

  • The shared persistClassifierResult() helper and the adoption of CLASSIFY_RESULT_PATH in both reply workflows correctly fix the live stdout-scraping bug that was stranding reporter replies in production.
  • Auth, shell-safety, dispatch wiring (repository_dispatch with JSON-escaped --rawfile), concurrency serialization per issue, and capability-gate bail wording are all correct.
  • No AGENTS.md product-code conventions are violated (this PR only touches .flue/.github).
  • No changeset is required — neither .flue nor .github is a published package.

Verdict

comment — one minor suggestion on fallback wording; the approach is right and the prior logic bug is fixed.

// Truthiness, not `??`: the orchestrator passes "" (not undefined) when
// there are no bot comments, and an empty section loses the model's cue.
payload.botContext?.trim() ||
"(unavailable; assume the bot reproduced the issue and either proposed options or pushed a candidate fix)",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] The fallback text is misleading for triage/by-design issues. This workflow fires on both triage/reproduced and triage/by-design, but the fallback says to "assume the bot reproduced the issue and either proposed options or pushed a candidate fix." On a by-design issue the bot concluded the behavior is intended, not that it reproduced a bug or proposed a fix. The fallback only triggers when botContext is empty (an edge case, since those labels are bot-set), but when it does fire, it gives the model the wrong context for by-design.

Suggested change
"(unavailable; assume the bot reproduced the issue and either proposed options or pushed a candidate fix)",
payload.botContext?.trim() ||
"(unavailable; assume the bot has already investigated this issue)",

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-rereview Author pushed changes since the last review labels Jun 3, 2026
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.
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jun 3, 2026
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.
@ascorbic ascorbic merged commit 9f21a2b into main Jun 3, 2026
51 of 54 checks passed
@ascorbic ascorbic deleted the feat/maintainer-reply branch June 3, 2026 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci bot:review Trigger an emdashbot code review on this PR cla: signed review/needs-rereview Author pushed changes since the last review size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants