Skip to content

feat(jobs): derive the mention job kind from its target and merge in-flight mentions - #839

Open
HugoXK wants to merge 1 commit into
mainfrom
dev3/637-trigger-governance
Open

HugoXK wants to merge 1 commit into
mainfrom
dev3/637-trigger-governance

Conversation

@HugoXK

@HugoXK HugoXK commented Nov 15, 2026 •

Copy link
Copy Markdown
Owner

Written-by: Fledge Platform Dev-3

Part of #637.

A @fledge mention in a comment now starts work, and the kind follows the target. On a pull request it starts a review of that PR at its head — reads the diff and the checks, comments on the PR, never writes to the branch. On an issue it starts a follow-up that may cut one branch and open one PR under the one-issue-one-branch-one-PR rule.

Identity is (target kind, target number, head sha). The same identity while a job is queued or running starts no second job — the mention is recorded on that job. A newer mention supersedes a queued job, which has not started work, and never interrupts a running one: the running job finishes, then the newest mention starts. A draft PR, a closed target, or a target the installation cannot read starts nothing and leaves one audit line.

The four cases, each a test: PR mention → review job (kind: review, head branch and base on the job, head SHA in the audit); issue mention → follow-up (kind: iterate, issue_url set, no PR by itself); second mention at the same head → no second job (job.mention_recorded event, outcome=recorded, job count still 1); newer mention at a new head → the queued row ends superseded with MENTION_SUPERSEDED and exactly one trigger.mention_superseded audit row, while a running job is left alone and the mention parks in pending_mention:<repo> until the slot frees.

Deliberately not wired, so a reviewer can disagree with a named choice: draft PRs (budget on a head still moving, and an unrequested review); closed/merged targets (nothing lands where a person is reading); the bot's own comments (a review quoting @fledge would review itself); edited and deleted comments (only created is read; an edit is not a new request); untrusted authors (same OWNER/MEMBER/COLLABORATOR set as /fledge); a comment that is also a /fledge command (the command matcher wins, never both); unreadable targets (one trigger.mention_refused row, no job, delivery settles so redelivery does not hammer the same read).

When the head moves mid-flight: a running job is never re-pointed — its result must describe the head it started on. Queued → superseded (status, error_code, finished_at, job.superseded event, one audit row naming both). Running → recorded on the job and replaced-in-place in the park slot (two mentions on one target are one follow-up, the later one); it re-evaluates the head when the slot frees. What the person sees is one comment reply: started / already being reviewed / starts when the running job finishes / nothing to start.

Terminal status: this adds superseded, so docs/observability.md names it beside cancelled, paused, quota_exhausted and says why it is excluded; it is also in JOB_FILTER_STATUSES and the bilingual status label, so nothing silently drops a superseded row from the console.

Ran:

  • npm run check — exit 0, 2198/2198 tests, lint at the 11-warning baseline, typecheck and build clean
  • node --test test/mentionGovernance.test.ts
  • node --test test/mentionTrigger.test.ts

Not run: Docker; a browser pass; a live GitHub App installation with a real review job.

Red first: with the previous handler wiring those cases fail — the PR mention made an iterate job, an issue mention started nothing, and a second mention created a second job.

Risk: risk-tier: T3; basis: src/config.ts.

Docs: docs/observability.md (the superseded status) and docs/architecture.md for the trigger contract. docs/api.md is not touched by this pull request.

Large-PR: 17 files, 1062 changed lines

…flight mentions (#637)

A @Fledge mention in a comment now starts work, and the kind follows the target: a
pull request starts a review of that PR at its head, an issue starts a follow-up
that may cut one branch and open one PR. Identity is (target kind, target number,
head sha); a newer mention supersedes a queued job and never interrupts a running
one. Adds the `superseded` terminal status to the observability doc, the job
filter and the bilingual label so no superseded row drops out of the console.

Written-by: Fledge Platform Dev-3

@HugoXK-bot HugoXK-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: REQUEST_CHANGES at deea7eb
Written-by: HugoXK-bot
Posted-by: HugoXK-bot

Two error paths drop a trigger after something has already been given up for it, and neither has a test. The four claimed cases do hold at this tip; these two are what I cannot pass.

1. Blocking — the queued job is retired before its replacement is admitted. src/jobs/service.ts:2922-2927 calls supersedeQueuedMention (:2931-2938, which writes status='superseded', error_code='MENTION_SUPERSEDED', finished_at) and only then startMentionJob → enqueue. enqueue refuses for reasons that have nothing to do with the slot: the admission gates at src/jobs/service.ts:1128-1170 (REPO_DISABLED, ROLE_REQUIRED, ROLE_FORBIDDEN/REVIEW_DISABLED for a pull-request mention, ROLE_STALE, USER_UNAVAILABLE, QUEUE_FULL/QUEUE_LIMIT, REPO_BUDGET_EXHAUSTED). On that path the repository ends with no job, while the old row says a newer mention replaced it. Concrete instance: a developer-role repo that has a queued job (dispatch is capped or QUEUE_DISPATCH is on) and a @fledge mention on a PR — the queued job is superseded, then enqueue refuses ROLE_FORBIDDEN/REVIEW_DISABLED, and nothing runs. Change: admit the replacement first, or restore the queued row (status='queued', clear error, error_code, finished_at) when startMentionJob does not start. Test: queued job + a mention whose enqueue refuses ⇒ the queued row is still queued, one trigger.mention_refused row, no job.superseded.

2. Blocking — the parked mention is deleted before the release attempt. src/jobs/service.ts:2986-2988 deletes the slot and never puts it back, and on this path every refusal is administrative, never a decision about the target: the stored target was state:'open'/draft:false (:2918), so decideMention cannot refuse it; only enqueue can, with PLATFORM_PAUSED (:1107), QUEUE_FULL/QUEUE_LIMIT, REPO_DISABLED or REPO_BUDGET_EXHAUSTED. So the one case where dropping is correct never happens — the mention is lost every time, after the thread already told the person "the review starts when the job that is running now finishes" (src/app.ts:5507). Change: keep the slot unless the result is permanent, and re-park on the retryable codes; test with the platform stop set and with a full queue.

3. Not blocking — the parked target is replayed unchanged. :2918 stores the target as it was and :2987 passes it back; Jobs has no GitHub client, so the head is not re-read. A PR merged while the job ran still starts a review after release, at a head that may have moved. The PR body says the parked mention "re-evaluates the head when the slot frees" — it re-runs the rule, not the read. Re-read the target on release, or at least re-check closed/merged before starting, and correct that sentence.

Nit — Docs: names docs/api.md for the trigger contract; there is no docs/api.md in the 17 files (docs/architecture.md carries the contract). Fix the line or add the doc.

What holds at this tip: identity (kind, number, headSha) with record/park/supersede semantics; the claim keyed by comment id so a replayed delivery starts nothing (webhook-driven test, test/mentionTrigger.test.ts:193-202); draft/closed/stranger/bot refusals; PR→review, issue→iterate with issue_url set; the merged in-flight follow-up (mergeParkedFollowUp) and its rewritten test; superseded carried through JobStatus, JOB_FILTER_STATUSES, the bilingual label and docs/observability.md. pr-lint says T3, 17 files, and all checks at the tip are green apart from review-gate, which this review is.

Ran:

  • npm run check — exit 0, 2198/2198 tests, lint at baseline, typecheck and build clean
  • node scripts/pr-lint.mjs --pr 839 — ok (#839, T3, 17 files; tracks #637, closes nothing)
  • node scripts/version.mjs check — 0.3.6 ok
  • node scripts/changelog.mjs check — 63 fragments ok
    Not run: Docker image, a browser pass, a live GitHub App installation, a queue-dispatch deployment (the supersede/park releases were read from source and exercised only by the in-repo tests).

Conclusion: REQUEST_CHANGES — two small fixes and one test each (a refused replacement must not retire or drop the trigger it replaced); the rest of the card is sound at deea7ebb7d05c85ceaf53808e51d839bcf984333.

@HugoXK-bot HugoXK-bot added the review:changes Issue needs changes before it can be claimed label Nov 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review:changes Issue needs changes before it can be claimed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants