feat(server): wake the agent when a Forgejo checkbox is toggled #230

Merged
lucas merged 1 commit from agent/issue-229-checkbox-trigger into main 2026-11-07 09:45:21 +01:00
Collaborator

Closes: #229

Ticking or unticking a box in a markdown checklist now wakes the agent — in a comment, in an inline review comment (which Forgejo delivers as an ordinary issue_comment edit), or in the issue/pull request description, including checklists the agent wrote itself.

What changed

Receiver (forgejo/webhook.ts)

  • For action: "edited" the acting principal is the editor (sender.login), not comment.user.login. An edit of the agent's own comment is therefore accepted (commentAuthor = the editor); only the agent editing is ignored, as "self-edited" — that is the loop guard, since the agent rewrites its own checklists. created is unchanged.
  • Issue/PR description edits are classified instead of rejected. A missing changes.body.from is "no body change", which is what separates a content edit from the title change Forgejo also sends as edited.
  • dedupeKey takes the edited subject (the comment, or the issue/PR for a description edit with comment id 0); successive toggles differ in body, so each toggle keeps its own key inside one second.

New pure module (forgejo/checkbox-diff.ts)

  • parseTaskItems, checklistBlocks, diffCheckboxes. Code fences and > quotes are skipped. Items are paired on the first word of their label, so a tick survives an accompanying prose edit; a pure reorder, and any edit leaving every item's first word and state alone, returns null (non-event). Lists that cannot be paired positionally yield flips: null and hand the agent both checklists rather than guessing.

Detection (forgejo/conversation-detect.ts)

  • New "checkbox-change" reason (also in CONVERSATION_REASONS, so it runs workspace-less). It requires that the agent took part in the artifact: it wrote the edited text, or it appears in the conversation (which includes the opening entry, so an artifact it opened counts). A failed participation check falls through to the existing edited-mention rule, so no behaviour is lost.
  • buildCheckboxEditSeed builds the turn: which items flipped, the checklist as it stands now, and an explicitly neutral closing paragraph — a tick implies nothing in particular; the agent decides from the conversation.

Dispatcher (forgejo/dispatcher.ts)

  • 15 s settle window (CHECKBOX_SETTLE_MS): the rendered widget posts one request per click, so toggles of the same text collapse into one turn. Siblings are read from the stored payloads at gate time, so a crash inside the window loses nothing. An edit with no checkbox change is never delayed.
  • The old "edited by someone other than its author" guard now applies only to edited rows that are not a checkbox change (for every other row commentAuthor is the sender, so the old comparison was a no-op there). A checkbox change is authorised in the editor's own name instead.
  • Permission: for a checkbox change only the repository owner or a write/admin user passes. The issue/PR-opener shortcut and the read fallback do not apply, so an outside contributor cannot drive the agent by ticking boxes in an issue they opened.

Verification

  • New src/forgejo/checkbox-diff.test.ts (tick, untick, reorder, prose-only edit, simultaneous tick+untick, inserted item, fenced/quoted checklists, checklistBlocks).
  • webhook.test.ts: edit of the agent's comment accepted with the editor as author; self-edited; description edit accepted with changes.body.from and ignored (no body change) without it; per-toggle dedupe keys. The old "unsupported action edited" assertions are replaced.
  • conversation-detect.test.ts: checkbox-change with an agent entry and with editedAuthor = agent; null without participation; edited-mention still wins when no checkbox moved.
  • dispatcher.test.ts: a tick in the agent's comment by the repo owner starts a turn whose seed carries Trigger: a checkbox was ticked or unticked and - [x] alpha; a read editor who opened the issue is dropped; edited-mention still runs; a non-author edit is still dropped; two pending toggles stay deferred inside the window and then run as one turn, the other row dropped with coalesced into ….
  • bun test (1242 tests) and bun run typecheck pass from the repository root.

Note on deployment

The receiver now classifies issues/pull_request edited deliveries, but whether this deployment's webhook has the Issues and Pull request events ticked lives in Forgejo, not in this repo — unverified here. If description-edit triggers never arrive, check the hook's event selection; comment edits ride on the already-subscribed comment events.

Closes: #229 Ticking or unticking a box in a markdown checklist now wakes the agent — in a comment, in an inline review comment (which Forgejo delivers as an ordinary `issue_comment` edit), or in the issue/pull request description, including checklists the agent wrote itself. ## What changed **Receiver** (`forgejo/webhook.ts`) - For `action: "edited"` the acting principal is the **editor** (`sender.login`), not `comment.user.login`. An edit of the agent's own comment is therefore accepted (`commentAuthor` = the editor); only the agent *editing* is ignored, as `"self-edited"` — that is the loop guard, since the agent rewrites its own checklists. `created` is unchanged. - Issue/PR description edits are classified instead of rejected. A missing `changes.body.from` is `"no body change"`, which is what separates a content edit from the title change Forgejo also sends as `edited`. - `dedupeKey` takes the edited subject (the comment, or the issue/PR for a description edit with comment id `0`); successive toggles differ in `body`, so each toggle keeps its own key inside one second. **New pure module** (`forgejo/checkbox-diff.ts`) - `parseTaskItems`, `checklistBlocks`, `diffCheckboxes`. Code fences and `>` quotes are skipped. Items are paired on the first word of their label, so a tick survives an accompanying prose edit; a pure reorder, and any edit leaving every item's first word and state alone, returns `null` (non-event). Lists that cannot be paired positionally yield `flips: null` and hand the agent both checklists rather than guessing. **Detection** (`forgejo/conversation-detect.ts`) - New `"checkbox-change"` reason (also in `CONVERSATION_REASONS`, so it runs workspace-less). It requires that the agent took part in the artifact: it wrote the edited text, or it appears in the conversation (which includes the opening entry, so an artifact it opened counts). A failed participation check falls through to the existing `edited-mention` rule, so no behaviour is lost. - `buildCheckboxEditSeed` builds the turn: which items flipped, the checklist as it stands now, and an explicitly neutral closing paragraph — a tick implies nothing in particular; the agent decides from the conversation. **Dispatcher** (`forgejo/dispatcher.ts`) - 15 s settle window (`CHECKBOX_SETTLE_MS`): the rendered widget posts one request per click, so toggles of the same text collapse into one turn. Siblings are read from the stored payloads at gate time, so a crash inside the window loses nothing. An edit with no checkbox change is never delayed. - The old "edited by someone other than its author" guard now applies only to `edited` rows that are *not* a checkbox change (for every other row `commentAuthor` is the sender, so the old comparison was a no-op there). A checkbox change is authorised in the editor's own name instead. - Permission: for a checkbox change only the repository owner or a `write`/`admin` user passes. The issue/PR-opener shortcut and the `read` fallback do not apply, so an outside contributor cannot drive the agent by ticking boxes in an issue they opened. ## Verification - New `src/forgejo/checkbox-diff.test.ts` (tick, untick, reorder, prose-only edit, simultaneous tick+untick, inserted item, fenced/quoted checklists, `checklistBlocks`). - `webhook.test.ts`: edit of the agent's comment accepted with the editor as author; `self-edited`; description edit accepted with `changes.body.from` and ignored (`no body change`) without it; per-toggle dedupe keys. The old `"unsupported action edited"` assertions are replaced. - `conversation-detect.test.ts`: `checkbox-change` with an agent entry and with `editedAuthor` = agent; `null` without participation; `edited-mention` still wins when no checkbox moved. - `dispatcher.test.ts`: a tick in the agent's comment by the repo owner starts a turn whose seed carries `Trigger: a checkbox was ticked or unticked` and `- [x] alpha`; a `read` editor who opened the issue is dropped; `edited-mention` still runs; a non-author edit is still dropped; two pending toggles stay deferred inside the window and then run as one turn, the other row dropped with `coalesced into …`. - `bun test` (1242 tests) and `bun run typecheck` pass from the repository root. ## Note on deployment The receiver now classifies `issues`/`pull_request` `edited` deliveries, but whether this deployment's webhook has the **Issues** and **Pull request** events ticked lives in Forgejo, not in this repo — unverified here. If description-edit triggers never arrive, check the hook's event selection; comment edits ride on the already-subscribed comment events.
Ticking or unticking a box in a markdown checklist now starts a turn,
whether the checklist sits in a comment, an inline review comment or the
issue/pull request description, and including checklists the agent wrote
itself.

An edit is attributed to its editor rather than to the author of the
edited text, so the agent's own comments can be ticked by others while
its own rewrites still stop the loop. Description edits are classified
instead of rejected, with a missing `changes.body.from` separating a
content edit from the title change Forgejo also sends as `edited`.

Checkbox state is compared across the edit: a reorder, or a prose edit
leaving every item's state alone, is not a change, and lists that cannot
be paired positionally hand the agent both checklists. The turn is
deliberately neutral about what a tick means. Because the rendered
widget posts one request per click, toggles of the same text collapse
into a single turn after a 15 second settle window. Only the repository
owner or a write/admin user can fire it, and only on an artifact the
agent already took part in.

Co-Authored-By: Claude Opus 5 via omp-deck
lucas approved these changes 2026-11-07 09:45:17 +01:00
lucas merged commit b82f78962c into main 2026-11-07 09:45:21 +01:00
lucas deleted branch agent/issue-229-checkbox-trigger 2026-11-07 09:45:22 +01:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
lucas/omp-deck!230
No description provided.